diff options
author | Tomas Popela <tpopela@redhat.com> | 2014-06-09 22:32:25 +0800 |
---|---|---|
committer | Tomas Popela <tpopela@redhat.com> | 2014-06-09 22:32:25 +0800 |
commit | 8650fb139a9143f04615de74ff569bce3e0c4ce3 (patch) | |
tree | 89a41d08f179a5359b8eaee0c9344b8a5bf07cb3 /e-util | |
parent | 04b7c97275ae420dca43f3e65c2ef54d02f01bdd (diff) | |
download | gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar.gz gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar.bz2 gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar.lz gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar.xz gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.tar.zst gsoc2013-evolution-8650fb139a9143f04615de74ff569bce3e0c4ce3.zip |
Bug 540362: [webkit-composer] Use webkit for composer
Merge wip/webkit-composer branch into master.
Diffstat (limited to 'e-util')
76 files changed, 29457 insertions, 2968 deletions
diff --git a/e-util/Makefile.am b/e-util/Makefile.am index 66cdc220e4..6d9499db27 100644 --- a/e-util/Makefile.am +++ b/e-util/Makefile.am @@ -40,6 +40,7 @@ errordir = $(privdatadir)/errors @EVO_PLUGIN_RULE@ ui_DATA = \ + e-html-editor-manager.ui \ e-send-options.ui \ e-table-config.ui \ e-timezone-dialog.ui \ @@ -60,6 +61,7 @@ noinst_PROGRAMS = \ test-category-completion \ test-contact-store \ test-dateedit \ + test-html-editor \ test-mail-signatures \ test-name-selector \ test-preferences-window \ @@ -99,7 +101,7 @@ libevolution_util_la_CPPFLAGS = \ $(EVOLUTION_DATA_SERVER_CFLAGS) \ $(GNOME_PLATFORM_CFLAGS) \ $(GEO_CFLAGS) \ - $(GTKHTML_CFLAGS) \ + $(ENCHANT_CFLAGS) \ $(GTKSPELL_CFLAGS) \ $(CODE_COVERAGE_CFLAGS) \ $(NULL) @@ -165,6 +167,8 @@ evolution_util_include_HEADERS = \ e-client-cache.h \ e-client-combo-box.h \ e-client-selector.h \ + e-color-chooser-widget.h \ + e-color-combo.h \ e-config.h \ e-contact-store.h \ e-data-capture.h \ @@ -173,6 +177,11 @@ evolution_util_include_HEADERS = \ e-destination-store.h \ e-dialog-utils.h \ e-dialog-widgets.h \ + e-emoticon-action.h \ + e-emoticon-chooser-menu.h \ + e-emoticon-chooser.h \ + e-emoticon-tool-button.h \ + e-emoticon.h \ e-event.h \ e-file-request.h \ e-file-utils.h \ @@ -187,9 +196,27 @@ evolution_util_include_HEADERS = \ e-filter-part.h \ e-filter-rule.h \ e-focus-tracker.h \ + e-html-editor-actions.h \ + e-html-editor-cell-dialog.h \ + e-html-editor-dialog.h \ + e-html-editor-find-dialog.h \ + e-html-editor-hrule-dialog.h \ + e-html-editor-image-dialog.h \ + e-html-editor-link-dialog.h \ + e-html-editor-page-dialog.h \ + e-html-editor-paragraph-dialog.h \ + e-html-editor-replace-dialog.h \ + e-html-editor-selection.h \ + e-html-editor-spell-check-dialog.h \ + e-html-editor-table-dialog.h \ + e-html-editor-text-dialog.h \ + e-html-editor-utils.h \ + e-html-editor-view.h \ + e-html-editor.h \ e-html-utils.h \ e-icon-factory.h \ e-image-chooser.h \ + e-image-chooser-dialog.h \ e-import-assistant.h \ e-import.h \ e-interval-chooser.h \ @@ -252,6 +279,8 @@ evolution_util_include_HEADERS = \ e-source-selector-dialog.h \ e-source-selector.h \ e-source-util.h \ + e-spell-checker.h \ + e-spell-dictionary.h \ e-spell-entry.h \ e-spell-text-view.h \ e-stock-request.h \ @@ -305,7 +334,6 @@ evolution_util_include_HEADERS = \ e-url-entry.h \ e-util-enums.h \ e-util-enumtypes.h \ - e-web-view-gtkhtml.h \ e-web-view-preview.h \ e-web-view.h \ e-widget-undo.h \ @@ -410,6 +438,8 @@ libevolution_util_la_SOURCES = \ e-client-cache.c \ e-client-combo-box.c \ e-client-selector.c \ + e-color-chooser-widget.c \ + e-color-combo.c \ e-config.c \ e-contact-store.c \ e-data-capture.c \ @@ -418,6 +448,11 @@ libevolution_util_la_SOURCES = \ e-destination-store.c \ e-dialog-utils.c \ e-dialog-widgets.c \ + e-emoticon-action.c \ + e-emoticon-chooser-menu.c \ + e-emoticon-chooser.c \ + e-emoticon-tool-button.c \ + e-emoticon.c \ e-event.c \ e-file-request.c \ e-file-utils.c \ @@ -432,9 +467,28 @@ libevolution_util_la_SOURCES = \ e-filter-part.c \ e-filter-rule.c \ e-focus-tracker.c \ + e-html-editor-actions.c \ + e-html-editor-cell-dialog.c \ + e-html-editor-dialog.c \ + e-html-editor-find-dialog.c \ + e-html-editor-hrule-dialog.c \ + e-html-editor-image-dialog.c \ + e-html-editor-link-dialog.c \ + e-html-editor-page-dialog.c \ + e-html-editor-paragraph-dialog.c \ + e-html-editor-private.h \ + e-html-editor-replace-dialog.c \ + e-html-editor-selection.c \ + e-html-editor-spell-check-dialog.c \ + e-html-editor-table-dialog.c \ + e-html-editor-text-dialog.c \ + e-html-editor-utils.c \ + e-html-editor-view.c \ + e-html-editor.c \ e-html-utils.c \ e-icon-factory.c \ e-image-chooser.c \ + e-image-chooser-dialog.c \ e-import-assistant.c \ e-import.c \ e-interval-chooser.c \ @@ -497,6 +551,8 @@ libevolution_util_la_SOURCES = \ e-source-selector-dialog.c \ e-source-selector.c \ e-source-util.c \ + e-spell-checker.c \ + e-spell-dictionary.c \ e-spell-entry.c \ e-spell-text-view.c \ e-stock-request.c \ @@ -547,7 +603,6 @@ libevolution_util_la_SOURCES = \ e-url-entry.c \ e-util-enumtypes.c \ e-util-private.h \ - e-web-view-gtkhtml.c \ e-web-view-preview.c \ e-web-view.c \ e-widget-undo.c \ @@ -590,7 +645,7 @@ libevolution_util_la_LIBADD = \ $(EVOLUTION_DATA_SERVER_LIBS) \ $(GNOME_PLATFORM_LIBS) \ $(GEO_LIBS) \ - $(GTKHTML_LIBS) \ + $(ENCHANT_LIBS) \ $(GTKSPELL_LIBS) \ $(INTLLIBS) \ $(MATH_LIB) \ @@ -625,6 +680,10 @@ test_dateedit_CPPFLAGS = $(TEST_CPPFLAGS) test_dateedit_SOURCES = test-dateedit.c test_dateedit_LDADD = $(TEST_LDADD) +test_html_editor_CPPFLAGS = $(TEST_CPPFLAGS) +test_html_editor_SOURCES = test-html-editor.c +test_html_editor_LDADD = $(TEST_LDADD) + test_mail_signatures_CPPFLAGS = $(TEST_CPPFLAGS) test_mail_signatures_SOURCES = test-mail-signatures.c test_mail_signatures_LDADD = $(TEST_LDADD) diff --git a/e-util/e-action-combo-box.c b/e-util/e-action-combo-box.c index 1b784b8ee8..33d678ab1e 100644 --- a/e-util/e-action-combo-box.c +++ b/e-util/e-action-combo-box.c @@ -104,10 +104,6 @@ action_combo_box_render_pixbuf (GtkCellLayout *layout, gboolean visible; gint width; - /* Do any of the actions have an icon? */ - if (!combo_box->priv->group_has_icons) - return; - gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1); /* A NULL action means the row is a separator. */ @@ -122,8 +118,12 @@ action_combo_box_render_pixbuf (GtkCellLayout *layout, "visible", &visible, NULL); - /* Keep the pixbuf renderer a fixed size for proper alignment. */ - gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, NULL); + /* If some action has an icon */ + if (combo_box->priv->group_has_icons) + /* Keep the pixbuf renderer a fixed size for proper alignment. */ + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, NULL); + else + width = 0; /* We can't set both "icon-name" and "stock-id" because setting * one unsets the other. So pick the one that has a non-NULL @@ -245,13 +245,25 @@ action_combo_box_update_model (EActionComboBox *combo_box) GtkRadioAction *action = list->data; GtkTreePath *path; GtkTreeIter iter; - gchar *icon_name; - gchar *stock_id; + gchar *icon_name = NULL; + gchar *stock_id = NULL; + gboolean visible = FALSE; gint value; - g_object_get ( - action, "icon-name", &icon_name, - "stock-id", &stock_id, NULL); + g_object_get (action, + "icon-name", &icon_name, + "stock-id", &stock_id, + "visible", &visible, + NULL); + + if (!visible) { + g_free (icon_name); + g_free (stock_id); + + list = g_slist_next (list); + continue; + } + combo_box->priv->group_has_icons |= (icon_name != NULL || stock_id != NULL); g_free (icon_name); @@ -583,3 +595,11 @@ e_action_combo_box_add_separator_after (EActionComboBox *combo_box, GTK_LIST_STORE (model), &iter, COLUMN_ACTION, NULL, COLUMN_SORT, (gfloat) action_value + 0.5, -1); } + +void +e_action_combo_box_update_model (EActionComboBox *combo_box) +{ + g_return_if_fail (E_IS_ACTION_COMBO_BOX (combo_box)); + + action_combo_box_update_model (combo_box); +} diff --git a/e-util/e-action-combo-box.h b/e-util/e-action-combo-box.h index f3a5a0b658..3a44ed730a 100644 --- a/e-util/e-action-combo-box.h +++ b/e-util/e-action-combo-box.h @@ -81,6 +81,7 @@ void e_action_combo_box_add_separator_before void e_action_combo_box_add_separator_after (EActionComboBox *combo_box, gint action_value); +void e_action_combo_box_update_model (EActionComboBox *combo_box); G_END_DECLS diff --git a/e-util/e-color-chooser-widget.c b/e-util/e-color-chooser-widget.c new file mode 100644 index 0000000000..5761ebf2ff --- /dev/null +++ b/e-util/e-color-chooser-widget.c @@ -0,0 +1,253 @@ + +/* e-color-chooser-widget.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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-color-chooser-widget.h" + +#include <glib/gi18n-lib.h> + +#define E_COLOR_CHOOSER_WIDGET_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_COLOR_CHOOSER_WIDGET, EColorChooserWidgetPrivate)) + +/** + * EColorChooserWidget: + * + * This widget wrapps around #GtkColorChooserWidget and allows the widget to be + * used as a delegate for #GtkComboBox for instance. + */ + +struct _EColorChooserWidgetPrivate { + gboolean showing_editor; +}; + +enum { + SIGNAL_EDITOR_ACTIVATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + EColorChooserWidget, + e_color_chooser_widget, + GTK_TYPE_COLOR_CHOOSER_WIDGET); + +/* UGLY UGLY UGLY! + * GtkColorChooserWidget sends "color-activated" signal + * only when user double-clicks the color. This is totally stupid + * and since we want to use it in a combobox-like widget, we need + * to be notified upon single click (which by default only selects the color). + * + * Unfortunatelly the GtkColorSwatch widget, which handles the button-press + * event is a non-public widget embedded within the GtkColorChooserWidget, + * so we can't just subclass it and fix the behavior. + * + * Here we override button_press_event of the GtkColorSwatch and manually + * emit the 'activate' signal on single click. This is stupid, ugly and I + * want to punch someone for such a stupid design... + */ +static gboolean +color_chooser_widget_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + if ((event->type == GDK_BUTTON_PRESS) && + (event->button == GDK_BUTTON_PRIMARY)) { + + g_signal_emit_by_name (widget, "activate"); + + return TRUE; + } + + return FALSE; +} + +static void +color_chooser_widget_color_activated (GtkColorChooser *chooser, + GdkRGBA *color, + gpointer user_data) +{ + /* Because we are simulating the double-click by accepting only + * single click, the color in the swatch is actually not selected, + * so we must do it manually */ + gtk_color_chooser_set_rgba (chooser, color); +} + +static gboolean +run_color_chooser_dialog (gpointer user_data) +{ + EColorChooserWidgetPrivate *priv; + GtkWidget *parent_window; + GtkWidget *parent_chooser; + GtkWidget *dialog; + GtkWidget *chooser; + + parent_chooser = user_data; + + g_object_set ( + G_OBJECT (parent_chooser), "show-editor", FALSE, NULL); + + parent_window = g_object_get_data (G_OBJECT (parent_chooser), "window"); + if (!parent_window) + parent_window = gtk_widget_get_toplevel (parent_chooser); + dialog = gtk_dialog_new_with_buttons ( + N_("Choose custom color"), + GTK_WINDOW (parent_window), + GTK_DIALOG_MODAL, + GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, + GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); + + chooser = gtk_color_chooser_widget_new (); + g_object_set (G_OBJECT (chooser), "show-editor", TRUE, NULL); + gtk_box_pack_start ( + GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), + chooser, TRUE, TRUE, 5); + + gtk_widget_show_all (chooser); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { + GdkRGBA color; + + gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (chooser), &color); + gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (parent_chooser), &color); + + g_signal_emit_by_name (parent_chooser, "color-activated", &color); + } + + gtk_widget_destroy (dialog); + + priv = E_COLOR_CHOOSER_WIDGET_GET_PRIVATE (parent_chooser); + priv->showing_editor = FALSE; + + return FALSE; +} + +static void +color_chooser_show_editor_notify_cb (EColorChooserWidget *chooser, + GParamSpec *pspec, + gpointer user_data) +{ + gboolean show_editor; + + g_object_get (G_OBJECT (chooser), "show-editor", &show_editor, NULL); + + /* Nothing to do here... */ + if ((show_editor == FALSE) || (chooser->priv->showing_editor == TRUE)) + return; + + chooser->priv->showing_editor = TRUE; + + /* Hide the editor - we don't want to display the single-color editor + * within this widget. We rather create a dialog window with the editor + * (we can't do it from this callback as Gtk would stop it in order to + * prevent endless recursion probably) */ + g_idle_add (run_color_chooser_dialog, chooser); + g_signal_emit (chooser, signals[SIGNAL_EDITOR_ACTIVATED], 0); +} + +void +e_color_chooser_widget_class_init (EColorChooserWidgetClass *class) +{ + g_type_class_add_private (class, sizeof (EColorChooserWidgetPrivate)); + + signals[SIGNAL_EDITOR_ACTIVATED] = g_signal_new ( + "editor-activated", + E_TYPE_COLOR_CHOOSER_WIDGET, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (EColorChooserWidgetClass, editor_activated), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +/* Recursively go through GtkContainers within the GtkColorChooserWidget + * and try to find GtkColorSwatch widget. */ +static GtkWidget * +find_swatch (GtkContainer *container) +{ + GList *children, *child; + + children = gtk_container_get_children (container); + for (child = children; child; child = g_list_next (child)) { + GtkWidget *widget = child->data; + GtkWidget *swatch; + + if (GTK_IS_CONTAINER (widget)) { + swatch = find_swatch (GTK_CONTAINER (widget)); + + if (swatch != NULL) { + g_list_free (children); + return swatch; + } + } + + if (g_strcmp0 (G_OBJECT_TYPE_NAME (widget), "GtkColorSwatch") == 0) { + g_list_free (children); + return widget; + } + } + + g_list_free (children); + + return NULL; +} + +void +e_color_chooser_widget_init (EColorChooserWidget *widget) +{ + GtkWidget *swatch; + + widget->priv = E_COLOR_CHOOSER_WIDGET_GET_PRIVATE (widget); + widget->priv->showing_editor = FALSE; + + swatch = find_swatch (GTK_CONTAINER (widget)); + + /* If swatch is NULL then GTK changed something and this widget + * becomes broken... */ + g_return_if_fail (swatch != NULL); + + if (swatch) { + GtkWidgetClass *swatch_class; + swatch_class = GTK_WIDGET_GET_CLASS (swatch); + swatch_class->button_press_event = color_chooser_widget_button_press_event; + } + + g_signal_connect ( + widget, "color-activated", + G_CALLBACK (color_chooser_widget_color_activated), NULL); + + g_signal_connect ( + widget, "notify::show-editor", + G_CALLBACK (color_chooser_show_editor_notify_cb), NULL); +} + +GtkWidget * +e_color_chooser_widget_new (void) +{ + return g_object_new ( + E_TYPE_COLOR_CHOOSER_WIDGET, + "show-editor", FALSE, + "use-alpha", FALSE, + NULL); +} diff --git a/e-util/e-color-chooser-widget.h b/e-util/e-color-chooser-widget.h new file mode 100644 index 0000000000..3b7c60de7f --- /dev/null +++ b/e-util/e-color-chooser-widget.h @@ -0,0 +1,71 @@ +/* e-color-chooser-widget.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_COLOR_CHOOSER_WIDGET_H +#define E_COLOR_CHOOSER_WIDGET_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_COLOR_CHOOSER_WIDGET \ + (e_color_chooser_widget_get_type ()) +#define E_COLOR_CHOOSER_WIDGET(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COLOR_CHOOSER_WIDGET, EColorChooserWidget)) +#define E_COLOR_CHOOSER_WIDGET_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_COLOR_CHOOSER_WIDGET, EColorChooserWidgetClass)) +#define E_IS_COLOR_CHOOSER_WIDGET(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_COLOR_CHOOSER_WIDGET)) +#define E_IS_COLOR_CHOOSER_WIDGET_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_COLOR_CHOOSER_WIDGET)) +#define E_COLOR_CHOOSER_WIDGET_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_COLOR_CHOOSER_WIDGET, EColorChooserWidgetClass)) + +G_BEGIN_DECLS + +typedef struct _EColorChooserWidget EColorChooserWidget; +typedef struct _EColorChooserWidgetClass EColorChooserWidgetClass; +typedef struct _EColorChooserWidgetPrivate EColorChooserWidgetPrivate; + +struct _EColorChooserWidget { + GtkColorChooserWidget parent; + EColorChooserWidgetPrivate *priv; +}; + +struct _EColorChooserWidgetClass { + GtkColorChooserWidgetClass parent_class; + + void (*editor_activated) (GtkColorChooserWidget *chooser); +}; + +GType e_color_chooser_widget_get_type (void) G_GNUC_CONST; +GtkWidget * e_color_chooser_widget_new (void); + +G_END_DECLS + +#endif /* E_COLOR_CHOOSER_WIDGET_H */ + diff --git a/e-util/e-color-combo.c b/e-util/e-color-combo.c new file mode 100644 index 0000000000..f2f46e4bec --- /dev/null +++ b/e-util/e-color-combo.c @@ -0,0 +1,976 @@ +/* e-color-combo.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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-color-combo.h" +#include "e-color-chooser-widget.h" + +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> +#include <cairo/cairo.h> +#include <alloca.h> + +#define E_COLOR_COMBO_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_COLOR_COMBO, EColorComboPrivate)) + +struct _EColorComboPrivate { + GtkWidget *color_frame; /* not referenced */ + GtkWidget *arrow; /* not referenced */ + + GtkWidget *window; + GtkWidget *default_button; /* not referenced */ + GtkWidget *chooser_widget; /* not referenced */ + + guint popup_shown : 1; + guint popup_in_progress : 1; + + GdkRGBA *current_color; + GdkRGBA *default_color; + gint default_transparent: 1; + + GList *palette; + + GdkDevice *grab_keyboard; + GdkDevice *grab_mouse; +}; + +enum { + PROP_0, + PROP_CURRENT_COLOR, + PROP_DEFAULT_COLOR, + PROP_DEFAULT_LABEL, + PROP_DEFAULT_TRANSPARENT, + PROP_PALETTE, + PROP_POPUP_SHOWN +}; + +enum { + ACTIVATED, + POPUP, + POPDOWN, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; +static GdkRGBA black = { 0, 0, 0, 1 }; + +static struct { + const gchar *color; + const gchar *tooltip; +} default_colors[] = { + + { "#000000", N_("black") }, + { "#993300", N_("light brown") }, + { "#333300", N_("brown gold") }, + { "#003300", N_("dark green #2") }, + { "#003366", N_("navy") }, + { "#000080", N_("dark blue") }, + { "#333399", N_("purple #2") }, + { "#333333", N_("very dark gray") }, + + { "#800000", N_("dark red") }, + { "#FF6600", N_("red-orange") }, + { "#808000", N_("gold") }, + { "#008000", N_("dark green") }, + { "#008080", N_("dull blue") }, + { "#0000FF", N_("blue") }, + { "#666699", N_("dull purple") }, + { "#808080", N_("dark grey") }, + + { "#FF0000", N_("red") }, + { "#FF9900", N_("orange") }, + { "#99CC00", N_("lime") }, + { "#339966", N_("dull green") }, + { "#33CCCC", N_("dull blue #2") }, + { "#3366FF", N_("sky blue #2") }, + { "#800080", N_("purple") }, + { "#969696", N_("gray") }, + + { "#FF00FF", N_("magenta") }, + { "#FFCC00", N_("bright orange") }, + { "#FFFF00", N_("yellow") }, + { "#00FF00", N_("green") }, + { "#00FFFF", N_("cyan") }, + { "#00CCFF", N_("bright blue") }, + { "#993366", N_("red purple") }, + { "#C0C0C0", N_("light grey") }, + + { "#FF99CC", N_("pink") }, + { "#FFCC99", N_("light orange") }, + { "#FFFF99", N_("light yellow") }, + { "#CCFFCC", N_("light green") }, + { "#CCFFFF", N_("light cyan") }, + { "#99CCFF", N_("light blue") }, + { "#CC99FF", N_("light purple") }, + { "#FFFFFF", N_("white") } +}; + +G_DEFINE_TYPE ( + EColorCombo, + e_color_combo, + GTK_TYPE_BUTTON); + +static void +color_combo_reposition_window (EColorCombo *combo) +{ + GdkScreen *screen; + GdkWindow *window; + GdkRectangle monitor; + GtkAllocation allocation; + gint monitor_num; + gint x, y, width, height; + + screen = gtk_widget_get_screen (GTK_WIDGET (combo)); + window = gtk_widget_get_window (GTK_WIDGET (combo)); + monitor_num = gdk_screen_get_monitor_at_window (screen, window); + gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); + + gdk_window_get_origin (window, &x, &y); + + if (!gtk_widget_get_has_window (GTK_WIDGET (combo))) { + gtk_widget_get_allocation (GTK_WIDGET (combo), &allocation); + x += allocation.x; + y += allocation.y; + } + + gtk_widget_get_allocation (combo->priv->window, &allocation); + width = allocation.width; + height = allocation.height; + + x = CLAMP (x, monitor.x, monitor.x + monitor.width - width); + y = CLAMP (y, monitor.y, monitor.y + monitor.height - height); + + gtk_window_move (GTK_WINDOW (combo->priv->window), x, y); +} + +static void +color_combo_popup (EColorCombo *combo) +{ + GdkWindow *window; + gboolean grab_status; + GdkDevice *device, *mouse, *keyboard; + guint32 activate_time; + + device = gtk_get_current_event_device (); + g_return_if_fail (device != NULL); + + if (!gtk_widget_get_realized (GTK_WIDGET (combo))) + return; + + if (combo->priv->popup_shown) + return; + + activate_time = gtk_get_current_event_time (); + if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD) { + keyboard = device; + mouse = gdk_device_get_associated_device (device); + } else { + keyboard = gdk_device_get_associated_device (device); + mouse = device; + } + + /* Position the window over the button. */ + color_combo_reposition_window (combo); + + /* Show the pop-up. */ + gtk_widget_show_all (combo->priv->window); + gtk_widget_grab_focus (combo->priv->window); + + /* Try to grab the pointer and keyboard. */ + window = gtk_widget_get_window (combo->priv->window); + grab_status = + (keyboard == NULL) || + (gdk_device_grab ( + keyboard, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, + NULL, activate_time) == GDK_GRAB_SUCCESS); + if (grab_status) { + grab_status = + (mouse == NULL) || + (gdk_device_grab ( + mouse, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK, + NULL, activate_time) == GDK_GRAB_SUCCESS); + if (!grab_status && keyboard) + gdk_device_ungrab (keyboard, activate_time); + } + + if (grab_status) { + gtk_device_grab_add (combo->priv->window, mouse, TRUE); + combo->priv->grab_keyboard = keyboard; + combo->priv->grab_mouse = mouse; + } else { + gtk_widget_hide (combo->priv->window); + } + + /* Always make sure the editor-mode is OFF */ + g_object_set ( + G_OBJECT (combo->priv->chooser_widget), + "show-editor", FALSE, NULL); +} + +static void +color_combo_popdown (EColorCombo *combo) +{ + if (!gtk_widget_get_realized (GTK_WIDGET (combo))) + return; + + if (!combo->priv->popup_shown) + return; + + /* Hide the pop-up. */ + gtk_device_grab_remove (combo->priv->window, combo->priv->grab_mouse); + gtk_widget_hide (combo->priv->window); + + if (combo->priv->grab_keyboard) + gdk_device_ungrab (combo->priv->grab_keyboard, GDK_CURRENT_TIME); + if (combo->priv->grab_mouse) + gdk_device_ungrab (combo->priv->grab_mouse, GDK_CURRENT_TIME); + + combo->priv->grab_keyboard = NULL; + combo->priv->grab_mouse = NULL; +} + +static gboolean +color_combo_window_button_press_event_cb (EColorCombo *combo, + GdkEvent *event, + gpointer user_data) +{ + GtkWidget *event_widget; + + event_widget = gtk_get_event_widget ((GdkEvent *) event); + + if (event_widget == combo->priv->window) + return TRUE; + + if (combo->priv->popup_shown == TRUE) + return FALSE; + + color_combo_popup (combo); + + combo->priv->popup_in_progress = TRUE; + + return TRUE; +} + +static gboolean +color_combo_window_button_release_event_cb (EColorCombo *combo, + GdkEvent *event, + gpointer user_data) +{ + gboolean popup_in_progress; + + popup_in_progress = combo->priv->popup_in_progress; + combo->priv->popup_in_progress = FALSE; + + if (popup_in_progress) + return FALSE; + + if (combo->priv->popup_shown) + goto popdown; + + return FALSE; + +popdown: + color_combo_popdown (combo); + + return TRUE; +} + +static void +color_combo_child_show_cb (EColorCombo *combo) +{ + combo->priv->popup_shown = TRUE; + g_object_notify (G_OBJECT (combo), "popup-shown"); +} + +static void +color_combo_child_hide_cb (EColorCombo *combo) +{ + combo->priv->popup_shown = FALSE; + g_object_notify (G_OBJECT (combo), "popup-shown"); +} + +static void +color_combo_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *natural_width) +{ + GtkWidgetClass *widget_class; + + widget_class = GTK_WIDGET_CLASS (e_color_combo_parent_class); + widget_class->get_preferred_width (widget, min_width, natural_width); + + /* Make sure the box with color sample is always visible */ + if (min_width) + *min_width += 20; + + if (natural_width) + *natural_width += 20; +} + +static gboolean +color_combo_button_press_event_cb (GtkWidget *widget, + GdkEventButton *event) +{ + EColorCombo *combo = E_COLOR_COMBO (widget); + GdkWindow *window; + gint x, y, width, height; + + window = gtk_widget_get_window (combo->priv->color_frame); + gdk_window_get_position (window, &x, &y); + /* Width - only width of the frame with color box */ + width = gtk_widget_get_allocated_width (combo->priv->color_frame); + + /* Height - height of the entire button (widget) */ + height = gtk_widget_get_allocated_height (widget); + + /* Check whether user clicked on the color frame - in such case + * apply the color immediatelly without displaying the popup widget */ + if ((event->x_root >= x) && (event->x_root <= x + width) && + (event->y_root >= y) && (event->y_root <= y + height)) { + GdkRGBA color; + + e_color_combo_get_current_color (combo, &color); + g_signal_emit (combo, signals[ACTIVATED], 0, &color); + + return TRUE; + } + + /* Otherwise display the popup widget */ + if (combo->priv->popup_shown) { + color_combo_popdown (combo); + } else { + color_combo_popup (combo); + } + + return FALSE; +} + +static void +color_combo_swatch_color_changed (EColorCombo *combo, + GdkRGBA *color, + gpointer user_data) +{ + g_signal_emit (combo, signals[ACTIVATED], 0, color); + + e_color_combo_set_current_color (combo, color); + + color_combo_popdown (combo); +} + +static void +color_combo_draw_frame_cb (GtkWidget *widget, + cairo_t *cr, + gpointer user_data) +{ + EColorCombo *combo = user_data; + GdkRGBA rgba; + GtkAllocation allocation; + gint height, width; + + e_color_combo_get_current_color (combo, &rgba); + + gtk_widget_get_allocation (widget, &allocation); + width = allocation.width; + height = allocation.height; + + cairo_rectangle (cr, 0, 0, width - 10, height); + cairo_set_source_rgb (cr, rgba.red, rgba.green, rgba.blue); + cairo_fill (cr); +} + +static void +color_combo_set_default_color_cb (EColorCombo *combo, + gpointer user_data) +{ + GdkRGBA color; + + e_color_combo_get_default_color (combo, &color); + e_color_combo_set_current_color (combo, &color); + + g_signal_emit (combo, signals[ACTIVATED], 0, &color); +} + +static void +color_combo_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_COLOR: + e_color_combo_set_current_color ( + E_COLOR_COMBO (object), + g_value_get_boxed (value)); + return; + + case PROP_DEFAULT_COLOR: + e_color_combo_set_default_color ( + E_COLOR_COMBO (object), + g_value_get_boxed (value)); + return; + + case PROP_DEFAULT_LABEL: + e_color_combo_set_default_label ( + E_COLOR_COMBO (object), + g_value_get_string (value)); + return; + + case PROP_DEFAULT_TRANSPARENT: + e_color_combo_set_default_transparent ( + E_COLOR_COMBO (object), + g_value_get_boolean (value)); + return; + + case PROP_PALETTE: + e_color_combo_set_palette ( + E_COLOR_COMBO (object), + g_value_get_object (value)); + return; + + case PROP_POPUP_SHOWN: + if (g_value_get_boolean (value)) + e_color_combo_popup ( + E_COLOR_COMBO (object)); + else + e_color_combo_popdown ( + E_COLOR_COMBO (object)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +color_combo_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EColorComboPrivate *priv; + GdkRGBA color; + + priv = E_COLOR_COMBO_GET_PRIVATE (object); + + switch (property_id) { + case PROP_CURRENT_COLOR: + e_color_combo_get_current_color ( + E_COLOR_COMBO (object), &color); + g_value_set_boxed (value, &color); + return; + + case PROP_DEFAULT_COLOR: + e_color_combo_get_default_color ( + E_COLOR_COMBO (object), &color); + g_value_set_boxed (value, &color); + return; + + case PROP_DEFAULT_LABEL: + g_value_set_string ( + value, e_color_combo_get_default_label ( + E_COLOR_COMBO (object))); + return; + + case PROP_DEFAULT_TRANSPARENT: + g_value_set_boolean ( + value, + e_color_combo_get_default_transparent ( + E_COLOR_COMBO (object))); + return; + + case PROP_PALETTE: + g_value_set_object ( + value, e_color_combo_get_palette ( + E_COLOR_COMBO (object))); + return; + + case PROP_POPUP_SHOWN: + g_value_set_boolean (value, priv->popup_shown); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +color_combo_dispose (GObject *object) +{ + EColorComboPrivate *priv; + + priv = E_COLOR_COMBO_GET_PRIVATE (object); + + if (priv->window != NULL) { + gtk_widget_destroy (priv->window); + priv->window = NULL; + } + + if (priv->current_color != NULL) { + gdk_rgba_free (priv->current_color); + priv->current_color = NULL; + } + + if (priv->default_color != NULL) { + gdk_rgba_free (priv->default_color); + priv->default_color = NULL; + } + + g_list_free_full (priv->palette, (GDestroyNotify) gdk_rgba_free); + priv->palette = NULL; + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_color_combo_parent_class)->dispose (object); +} + +static void +e_color_combo_class_init (EColorComboClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EColorComboPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = color_combo_set_property; + object_class->get_property = color_combo_get_property; + object_class->dispose = color_combo_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->get_preferred_width = color_combo_get_preferred_width; + widget_class->button_press_event = color_combo_button_press_event_cb; + + class->popup = color_combo_popup; + class->popdown = color_combo_popdown; + + g_object_class_install_property ( + object_class, + PROP_CURRENT_COLOR, + g_param_spec_boxed ( + "current-color", + "Current color", + "The currently selected color", + GDK_TYPE_RGBA, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DEFAULT_COLOR, + g_param_spec_boxed ( + "default-color", + "Default color", + "The color associated with the default button", + GDK_TYPE_RGBA, + G_PARAM_CONSTRUCT | + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DEFAULT_LABEL, + g_param_spec_string ( + "default-label", + "Default label", + "The label for the default button", + _("Default"), + G_PARAM_CONSTRUCT | + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_DEFAULT_TRANSPARENT, + g_param_spec_boolean ( + "default-transparent", + "Default is transparent", + "Whether the default color is transparent", + FALSE, + G_PARAM_CONSTRUCT | + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_PALETTE, + g_param_spec_pointer ( + "palette", + "Color palette", + "Custom color palette", + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_POPUP_SHOWN, + g_param_spec_boolean ( + "popup-shown", + "Popup shown", + "Whether the combo's dropdown is shown", + FALSE, + G_PARAM_READWRITE)); + + signals[ACTIVATED] = g_signal_new ( + "activated", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EColorComboClass, activated), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[POPUP] = g_signal_new ( + "popup", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EColorComboClass, popup), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[POPDOWN] = g_signal_new ( + "popdown", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EColorComboClass, popdown), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Down, GDK_MOD1_MASK, "popup", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_KP_Down, GDK_MOD1_MASK, "popup", 0); + + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Up, GDK_MOD1_MASK, "popdown", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_KP_Up, GDK_MOD1_MASK, "popdown", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Escape, 0, "popdown", 0); +} + +static void +e_color_combo_init (EColorCombo *combo) +{ + GtkWidget *container; + GtkWidget *toplevel; + GtkWidget *widget; + GList *palette; + guint ii; + + combo->priv = E_COLOR_COMBO_GET_PRIVATE (combo); + + widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_add (GTK_CONTAINER (combo), widget); + + container = widget; + + /* Build the combo button. */ + widget = gtk_frame_new (NULL); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + g_signal_connect ( + widget, "draw", + G_CALLBACK (color_combo_draw_frame_cb), combo); + combo->priv->color_frame = widget; /* do not reference */ + + widget = gtk_separator_new (GTK_ORIENTATION_VERTICAL); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + + widget = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, TRUE, 0); + combo->priv->arrow = widget; /* do not reference */ + + /* Build the drop-down menu */ + widget = gtk_window_new (GTK_WINDOW_POPUP); + gtk_container_set_border_width (GTK_CONTAINER (widget), 5); + gtk_window_set_resizable (GTK_WINDOW (widget), FALSE); + gtk_window_set_type_hint ( + GTK_WINDOW (widget), GDK_WINDOW_TYPE_HINT_COMBO); + combo->priv->window = g_object_ref_sink (widget); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (combo)); + if (GTK_IS_WINDOW (toplevel)) { + gtk_window_group_add_window ( + gtk_window_get_group (GTK_WINDOW (toplevel)), + GTK_WINDOW (widget)); + gtk_window_set_transient_for ( + GTK_WINDOW (widget), GTK_WINDOW (toplevel)); + } + + g_signal_connect_swapped ( + widget, "show", + G_CALLBACK (color_combo_child_show_cb), combo); + g_signal_connect_swapped ( + widget, "hide", + G_CALLBACK (color_combo_child_hide_cb), combo); + g_signal_connect_swapped ( + widget, "button-press-event", + G_CALLBACK (color_combo_window_button_press_event_cb), combo); + g_signal_connect_swapped ( + widget, "button-release-event", + G_CALLBACK (color_combo_window_button_release_event_cb), combo); + + container = widget; + + widget = gtk_grid_new (); + gtk_grid_set_row_spacing (GTK_GRID (widget), 5); + gtk_container_add (GTK_CONTAINER (container), widget); + + container = widget; + + widget = gtk_button_new (); + gtk_grid_attach (GTK_GRID (container), widget, 0, 0, 1, 1); + combo->priv->default_button = widget; /* do not reference */ + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (color_combo_set_default_color_cb), combo); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (color_combo_popdown), combo); + + widget = e_color_chooser_widget_new (); + g_object_set_data (G_OBJECT (widget), "window", combo->priv->window); + gtk_grid_attach (GTK_GRID (container), widget, 0, 1, 1, 1); + combo->priv->chooser_widget = widget; /* do not reference */ + + g_signal_connect_swapped ( + widget, "color-activated", + G_CALLBACK (color_combo_swatch_color_changed), combo); + g_signal_connect_swapped ( + widget, "editor-activated", + G_CALLBACK (color_combo_popdown), combo); + + palette = NULL; + for (ii = 0; ii < G_N_ELEMENTS (default_colors); ii++) { + GdkRGBA *color = g_new0 (GdkRGBA, 1); + gdk_rgba_parse (color, default_colors[ii].color); + + palette = g_list_prepend (palette, color); + } + palette = g_list_reverse (palette); + e_color_combo_set_palette (combo, palette); + g_list_free_full (palette, (GDestroyNotify) g_free); + + combo->priv->current_color = gdk_rgba_copy (&black); + combo->priv->default_color = gdk_rgba_copy (&black); +} + +GtkWidget * +e_color_combo_new (void) +{ + return g_object_new (E_TYPE_COLOR_COMBO, NULL); +} + +GtkWidget * +e_color_combo_new_defaults (GdkRGBA *default_color, + const gchar *default_label) +{ + g_return_val_if_fail (default_color != NULL, NULL); + g_return_val_if_fail (default_label != NULL, NULL); + + return g_object_new ( + E_TYPE_COLOR_COMBO, + "default-color", default_color, + "default-label", default_label, + NULL); +} + +void +e_color_combo_popup (EColorCombo *combo) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + g_signal_emit (combo, signals[POPUP], 0); +} + +void +e_color_combo_popdown (EColorCombo *combo) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + g_signal_emit (combo, signals[POPDOWN], 0); +} + +void +e_color_combo_get_current_color (EColorCombo *combo, + GdkRGBA *color) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + g_return_if_fail (color != NULL); + + color->red = combo->priv->current_color->red; + color->green = combo->priv->current_color->green; + color->blue = combo->priv->current_color->blue; + color->alpha = combo->priv->current_color->alpha; +} + +void +e_color_combo_set_current_color (EColorCombo *combo, + const GdkRGBA *color) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + if (color == NULL) + color = &black; + + if (combo->priv->current_color) { + + if (gdk_rgba_equal (color, combo->priv->current_color)) { + return; + } + + gdk_rgba_free (combo->priv->current_color); + } + + combo->priv->current_color = gdk_rgba_copy (color); + + gtk_color_chooser_set_rgba ( + GTK_COLOR_CHOOSER (combo->priv->chooser_widget), color); + gtk_widget_queue_draw (combo->priv->color_frame); + + g_object_notify (G_OBJECT (combo), "current-color"); +} + +void +e_color_combo_get_default_color (EColorCombo *combo, + GdkRGBA *color) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + g_return_if_fail (color != NULL); + + color->red = combo->priv->default_color->red; + color->green = combo->priv->default_color->green; + color->blue = combo->priv->default_color->blue; + color->alpha = combo->priv->default_color->alpha; +} + +void +e_color_combo_set_default_color (EColorCombo *combo, + const GdkRGBA *color) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + if (color == NULL) + color = &black; + + if (combo->priv->default_color) { + + if (gdk_rgba_equal (color, combo->priv->default_color)) { + return; + } + + gdk_rgba_free (combo->priv->default_color); + } + combo->priv->default_color = gdk_rgba_copy (color); + + gtk_color_chooser_set_rgba ( + GTK_COLOR_CHOOSER (combo->priv->chooser_widget), color); + + g_object_notify (G_OBJECT (combo), "default-color"); +} + +const gchar * +e_color_combo_get_default_label (EColorCombo *combo) +{ + g_return_val_if_fail (E_IS_COLOR_COMBO (combo), NULL); + + return gtk_button_get_label (GTK_BUTTON (combo->priv->default_button)); +} + +void +e_color_combo_set_default_label (EColorCombo *combo, + const gchar *text) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + gtk_button_set_label (GTK_BUTTON (combo->priv->default_button), text); + + g_object_notify (G_OBJECT (combo), "default-label"); +} + +gboolean +e_color_combo_get_default_transparent (EColorCombo *combo) +{ + g_return_val_if_fail (E_IS_COLOR_COMBO (combo), FALSE); + + return combo->priv->default_transparent; +} + +void +e_color_combo_set_default_transparent (EColorCombo *combo, + gboolean transparent) +{ + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + combo->priv->default_transparent = transparent; + + g_object_notify (G_OBJECT (combo), "default-transparent"); +} + +GList * +e_color_combo_get_palette (EColorCombo *combo) +{ + g_return_val_if_fail (E_IS_COLOR_COMBO (combo), NULL); + + return g_list_copy (combo->priv->palette); +} + +void +e_color_combo_set_palette (EColorCombo *combo, + GList *palette) +{ + gint ii, count, colors_per_line; + GList *iter; + GdkRGBA *colors; + + g_return_if_fail (E_IS_COLOR_COMBO (combo)); + + count = g_list_length (palette); + colors_per_line = (count % 10 == 0) ? 10 : 9; + + colors = g_malloc_n (count, sizeof (GdkRGBA)); + g_list_free_full (combo->priv->palette, (GDestroyNotify) gdk_rgba_free); + ii = 0; + combo->priv->palette = NULL; + for (iter = palette; iter; iter = g_list_next (iter)) { + combo->priv->palette = g_list_prepend ( + combo->priv->palette, gdk_rgba_copy (iter->data)); + + colors[ii] = *((GdkRGBA *) iter->data); + ii++; + } + combo->priv->palette = g_list_reverse (combo->priv->palette); + + gtk_color_chooser_add_palette ( + GTK_COLOR_CHOOSER (combo->priv->chooser_widget), + GTK_ORIENTATION_HORIZONTAL, 0, 0, NULL); + gtk_color_chooser_add_palette ( + GTK_COLOR_CHOOSER (combo->priv->chooser_widget), + GTK_ORIENTATION_HORIZONTAL, colors_per_line, count, colors); + g_free (colors); +} diff --git a/e-util/e-color-combo.h b/e-util/e-color-combo.h new file mode 100644 index 0000000000..41f7fd1574 --- /dev/null +++ b/e-util/e-color-combo.h @@ -0,0 +1,96 @@ +/* e-color-combo.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_COLOR_COMBO_H +#define E_COLOR_COMBO_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_COLOR_COMBO \ + (e_color_combo_get_type ()) +#define E_COLOR_COMBO(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COLOR_COMBO, EColorCombo)) +#define E_COLOR_COMBO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_COLOR_COMBO, EColorComboClass)) +#define E_IS_COLOR_COMBO(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_COLOR_COMBO)) +#define E_IS_COLOR_COMBO_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_COLOR_COMBO)) +#define E_COLOR_COMBO_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_COLOR_COMBO, EColorComboClass)) + +G_BEGIN_DECLS + +typedef struct _EColorCombo EColorCombo; +typedef struct _EColorComboClass EColorComboClass; +typedef struct _EColorComboPrivate EColorComboPrivate; + +struct _EColorCombo { + GtkButton parent; + EColorComboPrivate *priv; +}; + +struct _EColorComboClass { + GtkButtonClass parent_class; + + void (*popup) (EColorCombo *combo); + void (*popdown) (EColorCombo *combo); + void (*activated) (EColorCombo *combo, + GdkRGBA *color); +}; + +GType e_color_combo_get_type (void) G_GNUC_CONST; +GtkWidget * e_color_combo_new (void); +GtkWidget * e_color_combo_new_defaults (GdkRGBA *default_color, + const gchar *default_label); +void e_color_combo_popup (EColorCombo *combo); +void e_color_combo_popdown (EColorCombo *combo); +void e_color_combo_get_current_color (EColorCombo *combo, + GdkRGBA *rgba); +void e_color_combo_set_current_color (EColorCombo *combo, + const GdkRGBA *color); +void e_color_combo_get_default_color (EColorCombo *combo, + GdkRGBA *color); +void e_color_combo_set_default_color (EColorCombo *combo, + const GdkRGBA *default_color); +const gchar * e_color_combo_get_default_label (EColorCombo *combo); +void e_color_combo_set_default_label (EColorCombo *combo, + const gchar *text); +gboolean e_color_combo_get_default_transparent + (EColorCombo *combo); +void e_color_combo_set_default_transparent + (EColorCombo *combo, + gboolean transparent); +GList * e_color_combo_get_palette (EColorCombo *combo); +void e_color_combo_set_palette (EColorCombo *combo, + GList *palette); + +G_END_DECLS + +#endif /* E_COLOR_COMBO_H */ diff --git a/e-util/e-emoticon-action.c b/e-util/e-emoticon-action.c new file mode 100644 index 0000000000..0850d33f49 --- /dev/null +++ b/e-util/e-emoticon-action.c @@ -0,0 +1,278 @@ +/* + * e-emoticon-action.c + * + * Copyright (C) 2008 Novell, Inc. + * 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 "e-emoticon-action.h" + +#include "e-emoticon-chooser.h" +#include "e-emoticon-chooser-menu.h" +#include "e-emoticon-tool-button.h" + +#define E_EMOTICON_ACTION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_EMOTICON_ACTION, EEmoticonActionPrivate)) + +struct _EEmoticonActionPrivate { + GList *choosers; + EEmoticonChooser *current_chooser; +}; + +enum { + PROP_0, + PROP_CURRENT_FACE +}; + +/* Forward Declarations */ +static void e_emoticon_action_interface_init + (EEmoticonChooserInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EEmoticonAction, + e_emoticon_action, + GTK_TYPE_ACTION, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EMOTICON_CHOOSER, + e_emoticon_action_interface_init)) + +static void +emoticon_action_proxy_item_activated_cb (EEmoticonAction *action, + EEmoticonChooser *chooser) +{ + action->priv->current_chooser = chooser; + + g_signal_emit_by_name (action, "item-activated"); +} + +static void +emoticon_action_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_FACE: + e_emoticon_chooser_set_current_emoticon ( + E_EMOTICON_CHOOSER (object), + g_value_get_boxed (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +emoticon_action_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_FACE: + g_value_set_boxed ( + value, e_emoticon_chooser_get_current_emoticon ( + E_EMOTICON_CHOOSER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +emoticon_action_finalize (GObject *object) +{ + EEmoticonActionPrivate *priv; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (object); + + g_list_free (priv->choosers); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_emoticon_action_parent_class)->finalize (object); +} + +static void +emoticon_action_activate (GtkAction *action) +{ + EEmoticonActionPrivate *priv; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (action); + + priv->current_chooser = NULL; +} + +static GtkWidget * +emoticon_action_create_menu_item (GtkAction *action) +{ + GtkWidget *item; + GtkWidget *menu; + + item = gtk_image_menu_item_new (); + menu = gtk_action_create_menu (action); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), menu); + gtk_widget_show (menu); + + return item; +} + +static GtkWidget * +emoticon_action_create_tool_item (GtkAction *action) +{ + return GTK_WIDGET (e_emoticon_tool_button_new ()); +} + +static void +emoticon_action_connect_proxy (GtkAction *action, + GtkWidget *proxy) +{ + EEmoticonActionPrivate *priv; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (action); + + if (!E_IS_EMOTICON_CHOOSER (proxy)) + goto chainup; + + if (g_list_find (priv->choosers, proxy) != NULL) + goto chainup; + + g_signal_connect_swapped ( + proxy, "item-activated", + G_CALLBACK (emoticon_action_proxy_item_activated_cb), action); + +chainup: + /* Chain up to parent's connect_proxy() method. */ + GTK_ACTION_CLASS (e_emoticon_action_parent_class)-> + connect_proxy (action, proxy); +} + +static void +emoticon_action_disconnect_proxy (GtkAction *action, + GtkWidget *proxy) +{ + EEmoticonActionPrivate *priv; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (action); + + priv->choosers = g_list_remove (priv->choosers, proxy); + + /* Chain up to parent's disconnect_proxy() method. */ + GTK_ACTION_CLASS (e_emoticon_action_parent_class)-> + disconnect_proxy (action, proxy); +} + +static GtkWidget * +emoticon_action_create_menu (GtkAction *action) +{ + EEmoticonActionPrivate *priv; + GtkWidget *widget; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (action); + + widget = e_emoticon_chooser_menu_new (); + + g_signal_connect_swapped ( + widget, "item-activated", + G_CALLBACK (emoticon_action_proxy_item_activated_cb), action); + + priv->choosers = g_list_prepend (priv->choosers, widget); + + return widget; +} + +static EEmoticon * +emoticon_action_get_current_emoticon (EEmoticonChooser *chooser) +{ + EEmoticonActionPrivate *priv; + EEmoticon *emoticon = NULL; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (chooser); + + if (priv->current_chooser != NULL) + emoticon = e_emoticon_chooser_get_current_emoticon ( + priv->current_chooser); + + return emoticon; +} + +static void +emoticon_action_set_current_emoticon (EEmoticonChooser *chooser, + EEmoticon *emoticon) +{ + EEmoticonActionPrivate *priv; + GList *iter; + + priv = E_EMOTICON_ACTION_GET_PRIVATE (chooser); + + for (iter = priv->choosers; iter != NULL; iter = iter->next) { + EEmoticonChooser *proxy_chooser = iter->data; + + e_emoticon_chooser_set_current_emoticon (proxy_chooser, emoticon); + } +} + +static void +e_emoticon_action_class_init (EEmoticonActionClass *class) +{ + GObjectClass *object_class; + GtkActionClass *action_class; + + g_type_class_add_private (class, sizeof (EEmoticonAction)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = emoticon_action_set_property; + object_class->get_property = emoticon_action_get_property; + object_class->finalize = emoticon_action_finalize; + + action_class = GTK_ACTION_CLASS (class); + action_class->activate = emoticon_action_activate; + action_class->create_menu_item = emoticon_action_create_menu_item; + action_class->create_tool_item = emoticon_action_create_tool_item; + action_class->connect_proxy = emoticon_action_connect_proxy; + action_class->disconnect_proxy = emoticon_action_disconnect_proxy; + action_class->create_menu = emoticon_action_create_menu; + + g_object_class_override_property ( + object_class, PROP_CURRENT_FACE, "current-emoticon"); +} + +static void +e_emoticon_action_interface_init (EEmoticonChooserInterface *interface) +{ + interface->get_current_emoticon = emoticon_action_get_current_emoticon; + interface->set_current_emoticon = emoticon_action_set_current_emoticon; +} + +static void +e_emoticon_action_init (EEmoticonAction *action) +{ + action->priv = E_EMOTICON_ACTION_GET_PRIVATE (action); +} + +GtkAction * +e_emoticon_action_new (const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *stock_id) +{ + g_return_val_if_fail (name != NULL, NULL); + + return g_object_new ( + E_TYPE_EMOTICON_ACTION, "name", name, "label", label, + "tooltip", tooltip, "stock-id", stock_id, NULL); +} diff --git a/e-util/e-emoticon-action.h b/e-util/e-emoticon-action.h new file mode 100644 index 0000000000..0e450e8750 --- /dev/null +++ b/e-util/e-emoticon-action.h @@ -0,0 +1,73 @@ +/* + * e-emoticon-action.h + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_EMOTICON_ACTION_H +#define E_EMOTICON_ACTION_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_EMOTICON_ACTION \ + (e_emoticon_action_get_type ()) +#define E_EMOTICON_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_EMOTICON_ACTION, EEmoticonAction)) +#define E_EMOTICON_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_EMOTICON_ACTION, EEmoticonActionClass)) +#define E_IS_EMOTICON_ACTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_EMOTICON_ACTION)) +#define E_IS_EMOTICON_ACTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_EMOTICON_ACTION)) +#define E_EMOTICON_ACTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_EMOTICON_ACTION, EEmoticonActionClass)) + +G_BEGIN_DECLS + +typedef struct _EEmoticonAction EEmoticonAction; +typedef struct _EEmoticonActionClass EEmoticonActionClass; +typedef struct _EEmoticonActionPrivate EEmoticonActionPrivate; + +struct _EEmoticonAction { + GtkAction parent; + EEmoticonActionPrivate *priv; +}; + +struct _EEmoticonActionClass { + GtkActionClass parent_class; +}; + +GType e_emoticon_action_get_type (void) G_GNUC_CONST; +GtkAction * e_emoticon_action_new (const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *stock_id); + +G_END_DECLS + +#endif /* E_EMOTICON_ACTION_H */ diff --git a/e-util/e-emoticon-chooser-menu.c b/e-util/e-emoticon-chooser-menu.c new file mode 100644 index 0000000000..f2ed3376cf --- /dev/null +++ b/e-util/e-emoticon-chooser-menu.c @@ -0,0 +1,184 @@ +/* + * e-emoticon-chooser-menu.c + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-emoticon-chooser-menu.h" +#include "e-emoticon-chooser.h" + +#include <glib/gi18n-lib.h> + +enum { + PROP_0, + PROP_CURRENT_FACE +}; + +/* Forward Declarations */ +static void e_emoticon_chooser_menu_interface_init + (EEmoticonChooserInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EEmoticonChooserMenu, + e_emoticon_chooser_menu, + GTK_TYPE_MENU, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EMOTICON_CHOOSER, + e_emoticon_chooser_menu_interface_init)) + +static void +emoticon_chooser_menu_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_FACE: + e_emoticon_chooser_set_current_emoticon ( + E_EMOTICON_CHOOSER (object), + g_value_get_boxed (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +emoticon_chooser_menu_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_FACE: + g_value_set_boxed ( + value, + e_emoticon_chooser_get_current_emoticon ( + E_EMOTICON_CHOOSER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static EEmoticon * +emoticon_chooser_menu_get_current_emoticon (EEmoticonChooser *chooser) +{ + GtkWidget *item; + + item = gtk_menu_get_active (GTK_MENU (chooser)); + if (item == NULL) + return NULL; + + return g_object_get_data (G_OBJECT (item), "emoticon"); +} + +static void +emoticon_chooser_menu_set_current_emoticon (EEmoticonChooser *chooser, + EEmoticon *emoticon) +{ + GList *list, *iter; + + list = gtk_container_get_children (GTK_CONTAINER (chooser)); + + for (iter = list; iter != NULL; iter = iter->next) { + GtkWidget *item = iter->data; + EEmoticon *candidate; + + candidate = g_object_get_data (G_OBJECT (item), "emoticon"); + if (candidate == NULL) + continue; + + if (e_emoticon_equal (emoticon, candidate)) { + gtk_menu_shell_activate_item ( + GTK_MENU_SHELL (chooser), item, TRUE); + break; + } + } + + g_list_free (list); +} + +static void +e_emoticon_chooser_menu_class_init (EEmoticonChooserMenuClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = emoticon_chooser_menu_set_property; + object_class->get_property = emoticon_chooser_menu_get_property; + + g_object_class_override_property ( + object_class, PROP_CURRENT_FACE, "current-emoticon"); +} + +static void +e_emoticon_chooser_menu_interface_init (EEmoticonChooserInterface *interface) +{ + interface->get_current_emoticon = + emoticon_chooser_menu_get_current_emoticon; + interface->set_current_emoticon = + emoticon_chooser_menu_set_current_emoticon; +} + +static void +e_emoticon_chooser_menu_init (EEmoticonChooserMenu *chooser_menu) +{ + EEmoticonChooser *chooser; + GList *list, *iter; + + chooser = E_EMOTICON_CHOOSER (chooser_menu); + list = e_emoticon_chooser_get_items (); + + for (iter = list; iter != NULL; iter = iter->next) { + EEmoticon *emoticon = iter->data; + GtkWidget *item; + + /* To keep translated strings in subclasses */ + item = gtk_image_menu_item_new_with_mnemonic (_(emoticon->label)); + gtk_image_menu_item_set_image ( + GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_icon_name ( + emoticon->icon_name, GTK_ICON_SIZE_MENU)); + gtk_widget_show (item); + + g_object_set_data_full ( + G_OBJECT (item), "emoticon", + e_emoticon_copy (emoticon), + (GDestroyNotify) e_emoticon_free); + + g_signal_connect_swapped ( + item, "activate", + G_CALLBACK (e_emoticon_chooser_item_activated), + chooser); + + gtk_menu_shell_append (GTK_MENU_SHELL (chooser_menu), item); + } + + g_list_free (list); +} + +GtkWidget * +e_emoticon_chooser_menu_new (void) +{ + return g_object_new (E_TYPE_EMOTICON_CHOOSER_MENU, NULL); +} diff --git a/e-util/e-emoticon-chooser-menu.h b/e-util/e-emoticon-chooser-menu.h new file mode 100644 index 0000000000..d4f99543bf --- /dev/null +++ b/e-util/e-emoticon-chooser-menu.h @@ -0,0 +1,70 @@ +/* + * e-emoticon-chooser-menu.h + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_EMOTICON_CHOOSER_MENU_H +#define E_EMOTICON_CHOOSER_MENU_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_EMOTICON_CHOOSER_MENU \ + (e_emoticon_chooser_menu_get_type ()) +#define E_EMOTICON_CHOOSER_MENU(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_EMOTICON_CHOOSER_MENU, EEmoticonChooserMenu)) +#define E_EMOTICON_CHOOSER_MENU_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_EMOTICON_CHOOSER_MENU, EEmoticonChooserMenuClass)) +#define E_IS_EMOTICON_CHOOSER_MENU(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_EMOTICON_CHOOSER_MENU)) +#define E_IS_EMOTICON_CHOOSER_MENU_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_EMOTICON_CHOOSER_MENU)) +#define E_EMOTICON_CHOOSER_MENU_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_EMOTICON_CHOOSER_MENU, EEmoticonChooserMenuClass)) + +G_BEGIN_DECLS + +typedef struct _EEmoticonChooserMenu EEmoticonChooserMenu; +typedef struct _EEmoticonChooserMenuClass EEmoticonChooserMenuClass; +typedef struct _EEmoticonChooserMenuPrivate EEmoticonChooserMenuPrivate; + +struct _EEmoticonChooserMenu { + GtkMenu parent; +}; + +struct _EEmoticonChooserMenuClass { + GtkMenuClass parent_class; +}; + +GType e_emoticon_chooser_menu_get_type + (void) G_GNUC_CONST; +GtkWidget * e_emoticon_chooser_menu_new (void); + +G_END_DECLS + +#endif /* E_EMOTICON_CHOOSER_MENU_H */ diff --git a/e-util/e-emoticon-chooser.c b/e-util/e-emoticon-chooser.c new file mode 100644 index 0000000000..44ce06ba55 --- /dev/null +++ b/e-util/e-emoticon-chooser.c @@ -0,0 +1,178 @@ +/* + * e-emoticon-chooser.c + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-emoticon-chooser.h" + +#include <glib/gi18n-lib.h> + +/* Constant version of EEMoticon. */ +typedef struct { + const gchar *label; + const gchar *icon_name; + const gchar *text_face; +} ConstantEmoticon; + +static ConstantEmoticon available_emoticons[] = { + /* Translators: :-) */ + { N_("_Smile"), "face-smile", ":-)" }, + /* Translators: :-( */ + { N_("S_ad"), "face-sad", ":-(" }, + /* Translators: ;-) */ + { N_("_Wink"), "face-wink", ";-)" }, + /* Translators: :-P */ + { N_("Ton_gue"), "face-raspberry", ":-P" }, + /* Translators: :-)) */ + { N_("Laug_h"), "face-laugh", ":-))" }, + /* Translators: :-| */ + { N_("_Plain"), "face-plain", ":-|" }, + /* Translators: :-! */ + { N_("Smi_rk"), "face-smirk", ":-!" }, + /* Translators: :"-) */ + { N_("_Embarrassed"), "face-embarrassed", ":\"-)" }, + /* Translators: :-D */ + { N_("_Big Smile"), "face-smile-big", ":-D" }, + /* Translators: :-/ */ + { N_("Uncer_tain"), "face-uncertain", ":-/" }, + /* Translators: :-O */ + { N_("S_urprise"), "face-surprise", ":-O" }, + /* Translators: :-S */ + { N_("W_orried"), "face-worried", ":-S" }, + /* Translators: :-* */ + { N_("_Kiss"), "face-kiss", ":-*" }, + /* Translators: X-( */ + { N_("A_ngry"), "face-angry", "X-(" }, + /* Translators: B-) */ + { N_("_Cool"), "face-cool", "B-)" }, + /* Translators: O:-) */ + { N_("Ange_l"), "face-angel", "O:-)" }, + /* Translators: :'( */ + { N_("Cr_ying"), "face-crying", ":'(" }, + /* Translators: :-Q */ + { N_("S_ick"), "face-sick", ":-Q" }, + /* Translators: |-) */ + { N_("Tire_d"), "face-tired", "|-)" }, + /* Translators: >:-) */ + { N_("De_vilish"), "face-devilish", ">:-)" }, + /* Translators: :-(|) */ + { N_("_Monkey"), "face-monkey", ":-(|)" } +}; + +enum { + ITEM_ACTIVATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_INTERFACE ( + EEmoticonChooser, + e_emoticon_chooser, + G_TYPE_OBJECT) + +static void +e_emoticon_chooser_default_init (EEmoticonChooserInterface *interface) +{ + g_object_interface_install_property ( + interface, + g_param_spec_boxed ( + "current-emoticon", + "Current Emoticon", + "Currently selected emoticon", + E_TYPE_EMOTICON, + G_PARAM_READWRITE)); + + signals[ITEM_ACTIVATED] = g_signal_new ( + "item-activated", + G_TYPE_FROM_INTERFACE (interface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EEmoticonChooserInterface, item_activated), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +EEmoticon * +e_emoticon_chooser_get_current_emoticon (EEmoticonChooser *chooser) +{ + EEmoticonChooserInterface *interface; + + g_return_val_if_fail (E_IS_EMOTICON_CHOOSER (chooser), NULL); + + interface = E_EMOTICON_CHOOSER_GET_INTERFACE (chooser); + g_return_val_if_fail (interface->get_current_emoticon != NULL, NULL); + + return interface->get_current_emoticon (chooser); +} + +void +e_emoticon_chooser_set_current_emoticon (EEmoticonChooser *chooser, + EEmoticon *emoticon) +{ + EEmoticonChooserInterface *interface; + + g_return_if_fail (E_IS_EMOTICON_CHOOSER (chooser)); + + interface = E_EMOTICON_CHOOSER_GET_INTERFACE (chooser); + g_return_if_fail (interface->set_current_emoticon != NULL); + + interface->set_current_emoticon (chooser, emoticon); +} + +void +e_emoticon_chooser_item_activated (EEmoticonChooser *chooser) +{ + g_return_if_fail (E_IS_EMOTICON_CHOOSER (chooser)); + + g_signal_emit (chooser, signals[ITEM_ACTIVATED], 0); +} + +GList * +e_emoticon_chooser_get_items (void) +{ + GList *list = NULL; + gint ii; + + for (ii = 0; ii < G_N_ELEMENTS (available_emoticons); ii++) + list = g_list_prepend (list, &available_emoticons[ii]); + + return g_list_reverse (list); +} + +const EEmoticon * +e_emoticon_chooser_lookup_emoticon (const gchar *icon_name) +{ + gint ii; + + g_return_val_if_fail (icon_name && *icon_name, NULL); + + for (ii = 0; ii < G_N_ELEMENTS (available_emoticons); ii++) { + if (strcmp (available_emoticons[ii].icon_name, icon_name) == 0) { + return (const EEmoticon *) &available_emoticons[ii]; + } + } + + return NULL; +} + diff --git a/e-util/e-emoticon-chooser.h b/e-util/e-emoticon-chooser.h new file mode 100644 index 0000000000..14e899fd4e --- /dev/null +++ b/e-util/e-emoticon-chooser.h @@ -0,0 +1,77 @@ +/* + * e-emoticon-chooser.h + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_EMOTICON_CHOOSER_H +#define E_EMOTICON_CHOOSER_H + +#include <e-util/e-emoticon.h> + +/* Standard GObject macros */ +#define E_TYPE_EMOTICON_CHOOSER \ + (e_emoticon_chooser_get_type ()) +#define E_EMOTICON_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_EMOTICON_CHOOSER, EEmoticonChooser)) +#define E_IS_EMOTICON_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_EMOTICON_CHOOSER)) +#define E_EMOTICON_CHOOSER_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE \ + ((obj), E_TYPE_EMOTICON_CHOOSER, EEmoticonChooserInterface)) + +G_BEGIN_DECLS + +typedef struct _EEmoticonChooser EEmoticonChooser; +typedef struct _EEmoticonChooserInterface EEmoticonChooserInterface; + +struct _EEmoticonChooserInterface { + GTypeInterface parent_interface; + + /* Methods */ + EEmoticon * (*get_current_emoticon) (EEmoticonChooser *chooser); + void (*set_current_emoticon) (EEmoticonChooser *chooser, + EEmoticon *emoticon); + + /* Signals */ + void (*item_activated) (EEmoticonChooser *chooser); +}; + +GType e_emoticon_chooser_get_type (void) G_GNUC_CONST; +EEmoticon * e_emoticon_chooser_get_current_emoticon + (EEmoticonChooser *chooser); +void e_emoticon_chooser_set_current_emoticon + (EEmoticonChooser *chooser, + EEmoticon *emoticon); +void e_emoticon_chooser_item_activated + (EEmoticonChooser *chooser); + +GList * e_emoticon_chooser_get_items (void); +const EEmoticon * + e_emoticon_chooser_lookup_emoticon + (const gchar *icon_name); + +G_END_DECLS + +#endif /* E_EMOTICON_CHOOSER_H */ diff --git a/e-util/e-emoticon-tool-button.c b/e-util/e-emoticon-tool-button.c new file mode 100644 index 0000000000..54f99c94b0 --- /dev/null +++ b/e-util/e-emoticon-tool-button.c @@ -0,0 +1,695 @@ +/* + * e-emoticon-tool-button.c + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-emoticon-tool-button.h" + +/* XXX The "button" aspects of this widget are based heavily on the + * GtkComboBox tree-view implementation. Consider splitting it + * into a reusable "button-with-an-empty-window" widget. */ + +#include <string.h> +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> + +#include "e-emoticon-chooser.h" + +#define E_EMOTICON_TOOL_BUTTON_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_EMOTICON_TOOL_BUTTON, EEmoticonToolButtonPrivate)) + +/* XXX Should calculate this dynamically. */ +#define NUM_ROWS 7 +#define NUM_COLS 3 + +enum { + PROP_0, + PROP_CURRENT_EMOTICON, + PROP_POPUP_SHOWN +}; + +enum { + POPUP, + POPDOWN, + LAST_SIGNAL +}; + +struct _EEmoticonToolButtonPrivate { + GtkWidget *active_button; /* not referenced */ + GtkWidget *table; + GtkWidget *window; + + guint popup_shown : 1; + guint popup_in_progress : 1; + GdkDevice *grab_keyboard; + GdkDevice *grab_mouse; +}; + +static guint signals[LAST_SIGNAL]; + +/* Forward Declarations */ +static void e_emoticon_tool_button_interface_init + (EEmoticonChooserInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EEmoticonToolButton, + e_emoticon_tool_button, + GTK_TYPE_TOGGLE_TOOL_BUTTON, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EMOTICON_CHOOSER, + e_emoticon_tool_button_interface_init)) + +/* XXX Copied from _gtk_toolbar_elide_underscores() */ +static gchar * +emoticon_tool_button_elide_underscores (const gchar *original) +{ + gchar *q, *result; + const gchar *p, *end; + gsize len; + gboolean last_underscore; + + if (!original) + return NULL; + + len = strlen (original); + q = result = g_malloc (len + 1); + last_underscore = FALSE; + + end = original + len; + for (p = original; p < end; p++) { + if (!last_underscore && *p == '_') + last_underscore = TRUE; + else { + last_underscore = FALSE; + if (original + 2 <= p && p + 1 <= end && + p[-2] == '(' && p[-1] == '_' && + p[0] != '_' && p[1] == ')') { + q--; + *q = '\0'; + p++; + } else + *q++ = *p; + } + } + + if (last_underscore) + *q++ = '_'; + + *q = '\0'; + + return result; +} + +static void +emoticon_tool_button_reposition_window (EEmoticonToolButton *button) +{ + GdkScreen *screen; + GdkWindow *window; + GdkRectangle monitor; + GtkAllocation allocation; + gint monitor_num; + gint x, y, width, height; + + screen = gtk_widget_get_screen (GTK_WIDGET (button)); + window = gtk_widget_get_window (GTK_WIDGET (button)); + monitor_num = gdk_screen_get_monitor_at_window (screen, window); + gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); + + gdk_window_get_origin (window, &x, &y); + + if (!gtk_widget_get_has_window (GTK_WIDGET (button))) { + gtk_widget_get_allocation (GTK_WIDGET (button), &allocation); + x += allocation.x; + y += allocation.y; + } + + gtk_widget_get_allocation (button->priv->window, &allocation); + width = allocation.width; + height = allocation.height; + + x = CLAMP (x, monitor.x, monitor.x + monitor.width - width); + y = CLAMP (y, monitor.y, monitor.y + monitor.height - height); + + gtk_window_move (GTK_WINDOW (button->priv->window), x, y); +} + +static void +emoticon_tool_button_emoticon_clicked_cb (EEmoticonToolButton *button, + GtkWidget *emoticon_button) +{ + button->priv->active_button = emoticon_button; + e_emoticon_tool_button_popdown (button); +} + +static gboolean +emoticon_tool_button_emoticon_release_event_cb (EEmoticonToolButton *button, + GdkEventButton *event, + GtkButton *emoticon_button) +{ + GtkStateType state; + + state = gtk_widget_get_state (GTK_WIDGET (button)); + + if (state != GTK_STATE_NORMAL) + gtk_button_clicked (emoticon_button); + + return FALSE; +} + +static gboolean +emoticon_tool_button_button_release_event_cb (EEmoticonToolButton *button, + GdkEventButton *event) +{ + GtkToggleToolButton *tool_button; + GtkWidget *event_widget; + gboolean popup_in_progress; + + tool_button = GTK_TOGGLE_TOOL_BUTTON (button); + event_widget = gtk_get_event_widget ((GdkEvent *) event); + + popup_in_progress = button->priv->popup_in_progress; + button->priv->popup_in_progress = FALSE; + + if (event_widget != GTK_WIDGET (button)) + goto popdown; + + if (popup_in_progress) + return FALSE; + + if (gtk_toggle_tool_button_get_active (tool_button)) + goto popdown; + + return FALSE; + +popdown: + e_emoticon_tool_button_popdown (button); + + return TRUE; +} + +static void +emoticon_tool_button_child_show_cb (EEmoticonToolButton *button) +{ + button->priv->popup_shown = TRUE; + g_object_notify (G_OBJECT (button), "popup-shown"); +} + +static void +emoticon_tool_button_child_hide_cb (EEmoticonToolButton *button) +{ + button->priv->popup_shown = FALSE; + g_object_notify (G_OBJECT (button), "popup-shown"); +} + +static gboolean +emoticon_tool_button_child_key_press_event_cb (EEmoticonToolButton *button, + GdkEventKey *event) +{ + GtkWidget *window = button->priv->window; + + if (!gtk_bindings_activate_event (G_OBJECT (window), event)) + gtk_bindings_activate_event (G_OBJECT (button), event); + + return TRUE; +} + +static void +emoticon_tool_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CURRENT_EMOTICON: + e_emoticon_chooser_set_current_emoticon ( + E_EMOTICON_CHOOSER (object), + g_value_get_boxed (value)); + return; + + case PROP_POPUP_SHOWN: + if (g_value_get_boolean (value)) + e_emoticon_tool_button_popup ( + E_EMOTICON_TOOL_BUTTON (object)); + else + e_emoticon_tool_button_popdown ( + E_EMOTICON_TOOL_BUTTON (object)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +emoticon_tool_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EEmoticonToolButtonPrivate *priv; + + priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (object); + + switch (property_id) { + case PROP_CURRENT_EMOTICON: + g_value_set_boxed ( + value, + e_emoticon_chooser_get_current_emoticon ( + E_EMOTICON_CHOOSER (object))); + return; + + case PROP_POPUP_SHOWN: + g_value_set_boolean (value, priv->popup_shown); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +emoticon_tool_button_dispose (GObject *object) +{ + EEmoticonToolButtonPrivate *priv; + + priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (object); + + if (priv->window != NULL) { + gtk_widget_destroy (priv->window); + priv->window = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_emoticon_tool_button_parent_class)->dispose (object); +} + +static gboolean +emoticon_tool_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + EEmoticonToolButton *button; + GtkToggleToolButton *toggle_button; + GtkWidget *event_widget; + + button = E_EMOTICON_TOOL_BUTTON (widget); + + event_widget = gtk_get_event_widget ((GdkEvent *) event); + + if (event_widget == button->priv->window) + return TRUE; + + if (event_widget != widget) + return FALSE; + + toggle_button = GTK_TOGGLE_TOOL_BUTTON (widget); + if (gtk_toggle_tool_button_get_active (toggle_button)) + return FALSE; + + e_emoticon_tool_button_popup (button); + + button->priv->popup_in_progress = TRUE; + + return TRUE; +} + +static void +emoticon_tool_button_toggled (GtkToggleToolButton *button) +{ + if (gtk_toggle_tool_button_get_active (button)) + e_emoticon_tool_button_popup ( + E_EMOTICON_TOOL_BUTTON (button)); + else + e_emoticon_tool_button_popdown ( + E_EMOTICON_TOOL_BUTTON (button)); +} + +static void +emoticon_tool_button_popup (EEmoticonToolButton *button) +{ + GtkToggleToolButton *tool_button; + GdkWindow *window; + gboolean grab_status; + GdkDevice *device, *mouse, *keyboard; + guint32 activate_time; + + device = gtk_get_current_event_device (); + g_return_if_fail (device != NULL); + + if (!gtk_widget_get_realized (GTK_WIDGET (button))) + return; + + if (button->priv->popup_shown) + return; + + activate_time = gtk_get_current_event_time (); + if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD) { + keyboard = device; + mouse = gdk_device_get_associated_device (device); + } else { + keyboard = gdk_device_get_associated_device (device); + mouse = device; + } + + /* Position the window over the button. */ + emoticon_tool_button_reposition_window (button); + + /* Show the pop-up. */ + gtk_widget_show (button->priv->window); + gtk_widget_grab_focus (button->priv->window); + + /* Activate the tool button. */ + tool_button = GTK_TOGGLE_TOOL_BUTTON (button); + gtk_toggle_tool_button_set_active (tool_button, TRUE); + + /* Try to grab the pointer and keyboard. */ + window = gtk_widget_get_window (button->priv->window); + grab_status = !keyboard || + gdk_device_grab ( + keyboard, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, + NULL, activate_time) == GDK_GRAB_SUCCESS; + if (grab_status) { + grab_status = !mouse || + gdk_device_grab (mouse, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK, + NULL, activate_time) == GDK_GRAB_SUCCESS; + if (!grab_status && keyboard) + gdk_device_ungrab (keyboard, activate_time); + } + + if (grab_status) { + gtk_device_grab_add (button->priv->window, mouse, TRUE); + button->priv->grab_keyboard = keyboard; + button->priv->grab_mouse = mouse; + } else { + gtk_widget_hide (button->priv->window); + } +} + +static void +emoticon_tool_button_popdown (EEmoticonToolButton *button) +{ + GtkToggleToolButton *tool_button; + + if (!gtk_widget_get_realized (GTK_WIDGET (button))) + return; + + if (!button->priv->popup_shown) + return; + + /* Hide the pop-up. */ + gtk_device_grab_remove (button->priv->window, button->priv->grab_mouse); + gtk_widget_hide (button->priv->window); + + /* Deactivate the tool button. */ + tool_button = GTK_TOGGLE_TOOL_BUTTON (button); + gtk_toggle_tool_button_set_active (tool_button, FALSE); + + if (button->priv->grab_keyboard) + gdk_device_ungrab (button->priv->grab_keyboard, GDK_CURRENT_TIME); + if (button->priv->grab_mouse) + gdk_device_ungrab (button->priv->grab_mouse, GDK_CURRENT_TIME); + + button->priv->grab_keyboard = NULL; + button->priv->grab_mouse = NULL; +} + +static EEmoticon * +emoticon_tool_button_get_current_emoticon (EEmoticonChooser *chooser) +{ + EEmoticonToolButtonPrivate *priv; + + priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (chooser); + + if (priv->active_button == NULL) + return NULL; + + return g_object_get_data (G_OBJECT (priv->active_button), "emoticon"); +} + +static void +emoticon_tool_button_set_current_emoticon (EEmoticonChooser *chooser, + EEmoticon *emoticon) +{ + EEmoticonToolButtonPrivate *priv; + GList *list, *iter; + + priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (chooser); + + list = gtk_container_get_children (GTK_CONTAINER (priv->table)); + + for (iter = list; iter != NULL; iter = iter->next) { + GtkWidget *item = iter->data; + EEmoticon *candidate; + + candidate = g_object_get_data (G_OBJECT (item), "emoticon"); + if (candidate == NULL) + continue; + + if (e_emoticon_equal (emoticon, candidate)) { + gtk_button_clicked (GTK_BUTTON (item)); + break; + } + } + + g_list_free (list); +} + +static void +e_emoticon_tool_button_class_init (EEmoticonToolButtonClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkToggleToolButtonClass *toggle_tool_button_class; + + g_type_class_add_private (class, sizeof (EEmoticonToolButtonPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = emoticon_tool_button_set_property; + object_class->get_property = emoticon_tool_button_get_property; + object_class->dispose = emoticon_tool_button_dispose; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = emoticon_tool_button_press_event; + + toggle_tool_button_class = GTK_TOGGLE_TOOL_BUTTON_CLASS (class); + toggle_tool_button_class->toggled = emoticon_tool_button_toggled; + + class->popup = emoticon_tool_button_popup; + class->popdown = emoticon_tool_button_popdown; + + g_object_class_override_property ( + object_class, PROP_CURRENT_EMOTICON, "current-emoticon"); + + g_object_class_install_property ( + object_class, + PROP_POPUP_SHOWN, + g_param_spec_boolean ( + "popup-shown", + "Popup Shown", + "Whether the button's dropdown is shown", + FALSE, + G_PARAM_READWRITE)); + + signals[POPUP] = g_signal_new ( + "popup", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EEmoticonToolButtonClass, popup), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[POPDOWN] = g_signal_new ( + "popdown", + G_OBJECT_CLASS_TYPE (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EEmoticonToolButtonClass, popdown), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Down, GDK_MOD1_MASK, "popup", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_KP_Down, GDK_MOD1_MASK, "popup", 0); + + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Up, GDK_MOD1_MASK, "popdown", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_KP_Up, GDK_MOD1_MASK, "popdown", 0); + gtk_binding_entry_add_signal ( + gtk_binding_set_by_class (class), + GDK_KEY_Escape, 0, "popdown", 0); +} + +static void +e_emoticon_tool_button_interface_init (EEmoticonChooserInterface *interface) +{ + interface->get_current_emoticon = + emoticon_tool_button_get_current_emoticon; + interface->set_current_emoticon = + emoticon_tool_button_set_current_emoticon; +} + +static void +e_emoticon_tool_button_init (EEmoticonToolButton *button) +{ + EEmoticonChooser *chooser; + GtkWidget *toplevel; + GtkWidget *container; + GtkWidget *widget; + GtkWidget *window; + GList *list, *iter; + gint ii; + + button->priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (button); + + /* Build the pop-up window. */ + + window = gtk_window_new (GTK_WINDOW_POPUP); + gtk_window_set_resizable (GTK_WINDOW (window), FALSE); + gtk_window_set_type_hint ( + GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_COMBO); + button->priv->window = g_object_ref_sink (window); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (button)); + if (GTK_IS_WINDOW (toplevel)) { + gtk_window_group_add_window ( + gtk_window_get_group (GTK_WINDOW (toplevel)), + GTK_WINDOW (window)); + gtk_window_set_transient_for ( + GTK_WINDOW (window), GTK_WINDOW (toplevel)); + } + + g_signal_connect_swapped ( + window, "show", + G_CALLBACK (emoticon_tool_button_child_show_cb), button); + g_signal_connect_swapped ( + window, "hide", + G_CALLBACK (emoticon_tool_button_child_hide_cb), button); + g_signal_connect_swapped ( + window, "button-release-event", + G_CALLBACK (emoticon_tool_button_button_release_event_cb), + button); + g_signal_connect_swapped ( + window, "key-press-event", + G_CALLBACK (emoticon_tool_button_child_key_press_event_cb), + button); + + /* Build the pop-up window contents. */ + + widget = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_OUT); + gtk_container_add (GTK_CONTAINER (window), widget); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_table_new (NUM_ROWS, NUM_COLS, TRUE); + gtk_table_set_row_spacings (GTK_TABLE (widget), 0); + gtk_table_set_col_spacings (GTK_TABLE (widget), 0); + gtk_container_add (GTK_CONTAINER (container), widget); + button->priv->table = g_object_ref (widget); + gtk_widget_show (widget); + + container = widget; + + chooser = E_EMOTICON_CHOOSER (button); + list = e_emoticon_chooser_get_items (); + g_assert (g_list_length (list) <= NUM_ROWS * NUM_COLS); + + for (iter = list, ii = 0; iter != NULL; iter = iter->next, ii++) { + EEmoticon *emoticon = iter->data; + guint left = ii % NUM_COLS; + guint top = ii / NUM_COLS; + gchar *tooltip; + + tooltip = emoticon_tool_button_elide_underscores ( + gettext (emoticon->label)); + + widget = gtk_button_new (); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_icon_name ( + emoticon->icon_name, GTK_ICON_SIZE_BUTTON)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text (widget, tooltip); + gtk_widget_show (widget); + + g_object_set_data_full ( + G_OBJECT (widget), "emoticon", + e_emoticon_copy (emoticon), + (GDestroyNotify) e_emoticon_free); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (emoticon_tool_button_emoticon_clicked_cb), + button); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (e_emoticon_chooser_item_activated), + chooser); + + g_signal_connect_swapped ( + widget, "button-release-event", + G_CALLBACK (emoticon_tool_button_emoticon_release_event_cb), + button); + + gtk_table_attach ( + GTK_TABLE (container), widget, + left, left + 1, top, top + 1, 0, 0, 0, 0); + + g_free (tooltip); + } + + g_list_free (list); +} + +GtkToolItem * +e_emoticon_tool_button_new (void) +{ + return g_object_new (E_TYPE_EMOTICON_TOOL_BUTTON, NULL); +} + +void +e_emoticon_tool_button_popup (EEmoticonToolButton *button) +{ + g_return_if_fail (E_IS_EMOTICON_TOOL_BUTTON (button)); + + g_signal_emit (button, signals[POPUP], 0); +} + +void +e_emoticon_tool_button_popdown (EEmoticonToolButton *button) +{ + g_return_if_fail (E_IS_EMOTICON_TOOL_BUTTON (button)); + + g_signal_emit (button, signals[POPDOWN], 0); +} diff --git a/e-util/e-emoticon-tool-button.h b/e-util/e-emoticon-tool-button.h new file mode 100644 index 0000000000..fc7dc8ef64 --- /dev/null +++ b/e-util/e-emoticon-tool-button.h @@ -0,0 +1,75 @@ +/* + * e-emoticon-tool-button.h + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_EMOTICON_TOOL_BUTTON_H +#define E_EMOTICON_TOOL_BUTTON_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_EMOTICON_TOOL_BUTTON \ + (e_emoticon_tool_button_get_type ()) +#define E_EMOTICON_TOOL_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_EMOTICON_TOOL_BUTTON, EEmoticonToolButton)) +#define E_EMOTICON_TOOL_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_EMOTICON_TOOL_BUTTON, EEmoticonToolButtonClass)) +#define E_IS_EMOTICON_TOOL_BUTTON(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_EMOTICON_TOOL_BUTTON)) +#define E_IS_EMOTICON_TOOL_BUTTON_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_EMOTICON_TOOL_BUTTON)) +#define E_EMOTICON_TOOL_BUTTON_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_EMOTICON_TOOL_BUTTON, EEmoticonToolButtonClass)) + +G_BEGIN_DECLS + +typedef struct _EEmoticonToolButton EEmoticonToolButton; +typedef struct _EEmoticonToolButtonClass EEmoticonToolButtonClass; +typedef struct _EEmoticonToolButtonPrivate EEmoticonToolButtonPrivate; + +struct _EEmoticonToolButton { + GtkToggleToolButton parent; + EEmoticonToolButtonPrivate *priv; +}; + +struct _EEmoticonToolButtonClass { + GtkToggleToolButtonClass parent_class; + + void (*popup) (EEmoticonToolButton *button); + void (*popdown) (EEmoticonToolButton *button); +}; + +GType e_emoticon_tool_button_get_type (void) G_GNUC_CONST; +GtkToolItem * e_emoticon_tool_button_new (void); +void e_emoticon_tool_button_popup (EEmoticonToolButton *button); +void e_emoticon_tool_button_popdown (EEmoticonToolButton *button); + +G_END_DECLS + +#endif /* E_EMOTICON_TOOL_BUTTON_H */ diff --git a/e-util/e-emoticon.c b/e-util/e-emoticon.c new file mode 100644 index 0000000000..c543e52417 --- /dev/null +++ b/e-util/e-emoticon.c @@ -0,0 +1,118 @@ +/* + * e-emoticon.c + * + * Copyright (C) 2008 Novell, Inc. + * 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 "e-emoticon.h" + +#include <gtk/gtk.h> + +static EEmoticon * +emoticon_copy (EEmoticon *emoticon) +{ + EEmoticon *copy; + + copy = g_slice_new (EEmoticon); + copy->label = g_strdup (emoticon->label); + copy->icon_name = g_strdup (emoticon->icon_name); + copy->text_face = g_strdup (emoticon->text_face); + + return copy; +} + +static void +emoticon_free (EEmoticon *emoticon) +{ + g_free (emoticon->label); + g_free (emoticon->icon_name); + g_free (emoticon->text_face); + g_slice_free (EEmoticon, emoticon); +} + +GType +e_emoticon_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) + type = g_boxed_type_register_static ( + "EEmoticon", + (GBoxedCopyFunc) emoticon_copy, + (GBoxedFreeFunc) emoticon_free); + + return type; +} + +gboolean +e_emoticon_equal (EEmoticon *emoticon_a, + EEmoticon *emoticon_b) +{ + if (((emoticon_a == NULL) && (emoticon_b != NULL)) || + ((emoticon_a != NULL) && (emoticon_b == NULL))) + return FALSE; + + if (emoticon_a == emoticon_b) + return TRUE; + + if (g_strcmp0 (emoticon_a->label, emoticon_b->label) != 0) + return FALSE; + + if (g_strcmp0 (emoticon_a->icon_name, emoticon_b->icon_name) != 0) + return FALSE; + + if (g_strcmp0 (emoticon_a->text_face, emoticon_b->text_face) != 0) + return FALSE; + + return TRUE; +} + +EEmoticon * +e_emoticon_copy (EEmoticon *emoticon) +{ + return g_boxed_copy (E_TYPE_EMOTICON, emoticon); +} + +void +e_emoticon_free (EEmoticon *emoticon) +{ + g_boxed_free (E_TYPE_EMOTICON, emoticon); +} + +gchar * +e_emoticon_get_uri (EEmoticon *emoticon) +{ + GtkIconInfo *icon_info; + GtkIconTheme *icon_theme; + const gchar *filename; + gchar *uri = NULL; + + icon_theme = gtk_icon_theme_get_default (); + icon_info = gtk_icon_theme_lookup_icon ( + icon_theme, emoticon->icon_name, 16, 0); + g_return_val_if_fail (icon_info != NULL, NULL); + + filename = gtk_icon_info_get_filename (icon_info); + if (filename != NULL) { + uri = g_filename_to_uri (filename, NULL, NULL); + } + gtk_icon_info_free (icon_info); + g_return_val_if_fail (uri != NULL, NULL); + + return uri; +} diff --git a/e-util/e-emoticon.h b/e-util/e-emoticon.h new file mode 100644 index 0000000000..66327ab788 --- /dev/null +++ b/e-util/e-emoticon.h @@ -0,0 +1,53 @@ +/* + * e-emoticon.h + * + * Copyright (C) 2008 Novell, Inc. + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_EMOTICON_H +#define E_EMOTICON_H + +#include <glib-object.h> + +#define E_TYPE_EMOTICON \ + (e_emoticon_get_type ()) + +G_BEGIN_DECLS + +typedef struct _EEmoticon EEmoticon; + +struct _EEmoticon { + gchar *label; + gchar *icon_name; + gchar *text_face; +}; + +GType e_emoticon_get_type (void) G_GNUC_CONST; +gboolean e_emoticon_equal (EEmoticon *emoticon_a, + EEmoticon *emoticon_b); +EEmoticon * e_emoticon_copy (EEmoticon *emoticon); +void e_emoticon_free (EEmoticon *emoticon); +gchar * e_emoticon_get_uri (EEmoticon *emoticon); + +G_END_DECLS + +#endif /* E_EMOTICON_H */ diff --git a/e-util/e-focus-tracker.c b/e-util/e-focus-tracker.c index 0879a16c1d..3d3daec9fb 100644 --- a/e-util/e-focus-tracker.c +++ b/e-util/e-focus-tracker.c @@ -28,6 +28,7 @@ #include "e-selectable.h" #include "e-widget-undo.h" +#include "e-html-editor-view.h" #define E_FOCUS_TRACKER_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ @@ -255,6 +256,42 @@ focus_tracker_text_view_update_actions (EFocusTracker *focus_tracker, } static void +focus_tracker_editor_update_actions (EFocusTracker *focus_tracker, + EHTMLEditorView *view, + GdkAtom *targets, + gint n_targets) +{ + GtkAction *action; + gboolean can_copy; + gboolean can_cut; + gboolean can_paste; + + g_object_get (view, + "can-copy", &can_copy, + "can-cut", &can_cut, + "can-paste", &can_paste, + NULL); + + action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); + if (action != NULL) { + gtk_action_set_sensitive (action, can_cut); + gtk_action_set_tooltip (action, _("Cut the selection")); + } + + action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); + if (action != NULL) { + gtk_action_set_sensitive (action, can_copy); + gtk_action_set_tooltip (action, _("Copy the selection")); + } + + action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); + if (action != NULL) { + gtk_action_set_sensitive (action, can_paste); + gtk_action_set_tooltip (action, _("Paste the clipboard")); + } +} + +static void focus_tracker_selectable_update_actions (EFocusTracker *focus_tracker, ESelectable *selectable, GdkAtom *targets, @@ -331,6 +368,11 @@ focus_tracker_targets_received_cb (GtkClipboard *clipboard, focus_tracker, GTK_TEXT_VIEW (focus), targets, n_targets); + else if (E_IS_HTML_EDITOR_VIEW (focus)) + focus_tracker_editor_update_actions ( + focus_tracker, E_HTML_EDITOR_VIEW (focus), + targets, n_targets); + g_object_unref (focus_tracker); } @@ -349,6 +391,9 @@ focus_tracker_set_focus_cb (GtkWindow *window, if (GTK_IS_TEXT_VIEW (focus)) break; + if (E_IS_HTML_EDITOR_VIEW (focus)) + break; + focus = gtk_widget_get_parent (focus); } diff --git a/e-util/e-html-editor-actions.c b/e-util/e-html-editor-actions.c new file mode 100644 index 0000000000..6db3ad48da --- /dev/null +++ b/e-util/e-html-editor-actions.c @@ -0,0 +1,2028 @@ +/* e-html-editor-actions.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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gio/gio.h> +#include <glib/gi18n-lib.h> +#include <string.h> +#include <enchant/enchant.h> + +#include "e-html-editor.h" +#include "e-html-editor-private.h" +#include "e-html-editor-actions.h" +#include "e-html-editor-utils.h" +#include "e-emoticon-action.h" +#include "e-emoticon-chooser.h" +#include "e-image-chooser-dialog.h" +#include "e-spell-checker.h" + +static void +insert_html_file_ready_cb (GFile *file, + GAsyncResult *result, + EHTMLEditor *editor) +{ + EHTMLEditorSelection *selection; + gchar *contents = NULL; + gsize length; + GError *error = NULL; + + g_file_load_contents_finish ( + file, result, &contents, &length, NULL, &error); + if (error != NULL) { + GtkWidget *dialog; + + dialog = gtk_message_dialog_new ( + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (editor))), + 0, GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, _("Failed to insert HTML file.")); + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s.", error->message); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + g_clear_error (&error); + g_object_unref (editor); + return; + } + + selection = e_html_editor_view_get_selection ( + e_html_editor_get_view (editor)); + e_html_editor_selection_insert_html (selection, contents); + g_free (contents); + + g_object_unref (editor); +} + +static void +insert_text_file_ready_cb (GFile *file, + GAsyncResult *result, + EHTMLEditor *editor) +{ + EHTMLEditorSelection *selection; + gchar *contents; + gsize length; + GError *error = NULL; + + g_file_load_contents_finish ( + file, result, &contents, &length, NULL, &error); + if (error != NULL) { + GtkWidget *dialog; + + dialog = gtk_message_dialog_new ( + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (editor))), + 0, GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, _("Failed to insert text file.")); + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s.", error->message); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + g_clear_error (&error); + g_object_unref (editor); + return; + } + + selection = e_html_editor_view_get_selection ( + e_html_editor_get_view (editor)); + e_html_editor_selection_insert_text (selection, contents); + g_free (contents); + + g_object_unref (editor); +} + +static void +editor_update_static_spell_actions (EHTMLEditor *editor) +{ + ESpellChecker *checker; + EHTMLEditorView *view; + guint count; + + view = e_html_editor_get_view (editor); + checker = e_html_editor_view_get_spell_checker (view); + + count = e_spell_checker_count_active_languages (checker); + + gtk_action_set_visible (ACTION (CONTEXT_SPELL_ADD), count == 1); + gtk_action_set_visible (ACTION (CONTEXT_SPELL_ADD_MENU), count > 1); + gtk_action_set_visible (ACTION (CONTEXT_SPELL_IGNORE), count > 0); + + gtk_action_set_sensitive (ACTION (SPELL_CHECK), count > 0); +} + +/***************************************************************************** + * Action Callbacks + *****************************************************************************/ + +static void +action_context_delete_cell_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMNode *sibling; + WebKitDOMElement *cell; + + g_return_if_fail (editor->priv->table_cell != NULL); + + cell = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TD"); + if (!cell) { + cell = e_html_editor_dom_node_find_parent_element ( + editor->priv->table_cell, "TH"); + } + g_return_if_fail (cell != NULL); + + sibling = webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (cell)); + if (!sibling) { + sibling = webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (cell)); + } + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (cell)), + WEBKIT_DOM_NODE (cell), NULL); + + if (sibling) { + webkit_dom_html_table_cell_element_set_col_span ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (sibling), + webkit_dom_html_table_cell_element_get_col_span ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (sibling)) + 1); + } +} + +static void +action_context_delete_column_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *cell, *table; + WebKitDOMHTMLCollection *rows; + gulong index, length, ii; + + g_return_if_fail (editor->priv->table_cell != NULL); + + /* Find TD in which the selection starts */ + cell = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TD"); + if (!cell) { + cell = e_html_editor_dom_node_find_parent_element ( + editor->priv->table_cell, "TH"); + } + g_return_if_fail (cell != NULL); + + table = e_html_editor_dom_node_find_parent_element (WEBKIT_DOM_NODE (cell), "TABLE"); + g_return_if_fail (table != NULL); + + rows = webkit_dom_html_table_element_get_rows ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table)); + length = webkit_dom_html_collection_get_length (rows); + + index = webkit_dom_html_table_cell_element_get_cell_index ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (cell)); + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *row; + + row = webkit_dom_html_collection_item (rows, ii); + + webkit_dom_html_table_row_element_delete_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row), index, NULL); + } +} + +static void +action_context_delete_row_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *row; + + g_return_if_fail (editor->priv->table_cell != NULL); + + row = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TR"); + g_return_if_fail (row != NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (row)), + WEBKIT_DOM_NODE (row), NULL); +} + +static void +action_context_delete_table_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *table; + + g_return_if_fail (editor->priv->table_cell != NULL); + + table = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TABLE"); + g_return_if_fail (table != NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (table)), + WEBKIT_DOM_NODE (table), NULL); +} + +static void +action_context_insert_column_after_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *cell, *row; + gulong index; + + g_return_if_fail (editor->priv->table_cell != NULL); + + cell = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TD"); + if (!cell) { + cell = e_html_editor_dom_node_find_parent_element ( + editor->priv->table_cell, "TH"); + } + g_return_if_fail (cell != NULL); + + row = e_html_editor_dom_node_find_parent_element (WEBKIT_DOM_NODE (cell), "TR"); + g_return_if_fail (row != NULL); + + /* Get the first row in the table */ + row = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_first_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (row)))); + + index = webkit_dom_html_table_cell_element_get_cell_index ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (cell)); + + while (row) { + webkit_dom_html_table_row_element_insert_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row), index + 1, NULL); + + row = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (row))); + } +} + +static void +action_context_insert_column_before_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *cell, *row; + gulong index; + + g_return_if_fail (editor->priv->table_cell != NULL); + + cell = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TD"); + if (!cell) { + cell = e_html_editor_dom_node_find_parent_element ( + editor->priv->table_cell, "TH"); + } + g_return_if_fail (cell != NULL); + + row = e_html_editor_dom_node_find_parent_element (WEBKIT_DOM_NODE (cell), "TR"); + g_return_if_fail (row != NULL); + + /* Get the first row in the table */ + row = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_first_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (row)))); + + index = webkit_dom_html_table_cell_element_get_cell_index ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (cell)); + + while (row) { + webkit_dom_html_table_row_element_insert_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row), index - 1, NULL); + + row = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (row))); + } +} + +static void +action_context_insert_row_above_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *row, *table; + WebKitDOMHTMLCollection *cells; + WebKitDOMHTMLElement *new_row; + gulong index, cell_count, ii; + + g_return_if_fail (editor->priv->table_cell != NULL); + + row = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TR"); + g_return_if_fail (row != NULL); + + table = e_html_editor_dom_node_find_parent_element (WEBKIT_DOM_NODE (row), "TABLE"); + g_return_if_fail (table != NULL); + + index = webkit_dom_html_table_row_element_get_row_index ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + + new_row = webkit_dom_html_table_element_insert_row ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table), index, NULL); + + cells = webkit_dom_html_table_row_element_get_cells ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + cell_count = webkit_dom_html_collection_get_length (cells); + for (ii = 0; ii < cell_count; ii++) { + webkit_dom_html_table_row_element_insert_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (new_row), -1, NULL); + } + +} + +static void +action_context_insert_row_below_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitDOMElement *row, *table; + WebKitDOMHTMLCollection *cells; + WebKitDOMHTMLElement *new_row; + gulong index, cell_count, ii; + + g_return_if_fail (editor->priv->table_cell != NULL); + + row = e_html_editor_dom_node_find_parent_element (editor->priv->table_cell, "TR"); + g_return_if_fail (row != NULL); + + table = e_html_editor_dom_node_find_parent_element (WEBKIT_DOM_NODE (row), "TABLE"); + g_return_if_fail (table != NULL); + + index = webkit_dom_html_table_row_element_get_row_index ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + + new_row = webkit_dom_html_table_element_insert_row ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table), index + 1, NULL); + + cells = webkit_dom_html_table_row_element_get_cells ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + cell_count = webkit_dom_html_collection_get_length (cells); + for (ii = 0; ii < cell_count; ii++) { + webkit_dom_html_table_row_element_insert_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (new_row), -1, NULL); + } +} + +static void +action_context_remove_link_cb (GtkAction *action, + EHTMLEditor *editor) +{ + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_unlink (selection); +} + +static void +action_context_spell_add_cb (GtkAction *action, + EHTMLEditor *editor) +{ + ESpellChecker *spell_checker; + EHTMLEditorSelection *selection; + gchar *word; + + spell_checker = e_html_editor_view_get_spell_checker ( + editor->priv->html_editor_view); + selection = e_html_editor_view_get_selection (editor->priv->html_editor_view); + + word = e_html_editor_selection_get_caret_word (selection); + if (word && *word) { + e_spell_checker_learn_word (spell_checker, word); + } +} + +static void +action_context_spell_ignore_cb (GtkAction *action, + EHTMLEditor *editor) +{ + ESpellChecker *spell_checker; + EHTMLEditorSelection *selection; + gchar *word; + + spell_checker = e_html_editor_view_get_spell_checker ( + editor->priv->html_editor_view); + selection = e_html_editor_view_get_selection (editor->priv->html_editor_view); + + word = e_html_editor_selection_get_caret_word (selection); + if (word && *word) { + e_spell_checker_ignore_word (spell_checker, word); + } +} + +static void +action_copy_cb (GtkAction *action, + EHTMLEditor *editor) +{ + webkit_web_view_copy_clipboard ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); +} + +static void +action_cut_cb (GtkAction *action, + EHTMLEditor *editor) +{ + webkit_web_view_cut_clipboard ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); +} + +static void +action_indent_cb (GtkAction *action, + EHTMLEditor *editor) +{ + e_html_editor_selection_indent (editor->priv->selection); +} + +static void +action_insert_emoticon_cb (GtkAction *action, + EHTMLEditor *editor) +{ + EHTMLEditorView *view; + EEmoticon *emoticon; + + emoticon = e_emoticon_chooser_get_current_emoticon ( + E_EMOTICON_CHOOSER (action)); + g_return_if_fail (emoticon != NULL); + + view = e_html_editor_get_view (editor); + e_html_editor_view_insert_smiley (view, emoticon); +} + +static void +action_insert_html_file_cb (GtkToggleAction *action, + EHTMLEditor *editor) +{ + GtkWidget *dialog; + GtkFileFilter *filter; + + dialog = gtk_file_chooser_dialog_new ( + _("Insert HTML File"), NULL, + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("HTML file")); + gtk_file_filter_add_mime_type (filter, "text/html"); + gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { + GFile *file; + + file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); + + /* XXX Need a way to cancel this. */ + g_file_load_contents_async ( + file, NULL, (GAsyncReadyCallback) + insert_html_file_ready_cb, + g_object_ref (editor)); + + g_object_unref (file); + } + + gtk_widget_destroy (dialog); +} + +static void +action_insert_image_cb (GtkAction *action, + EHTMLEditor *editor) +{ + GtkWidget *dialog; + + dialog = e_image_chooser_dialog_new (_("Insert Image"), NULL); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + gchar *uri; + + uri = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog)); + + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + e_html_editor_selection_insert_image (selection, uri); + + g_free (uri); + } + + gtk_widget_destroy (dialog); +} + +static void +action_insert_link_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->link_dialog == NULL) + editor->priv->link_dialog = + e_html_editor_link_dialog_new (editor); + + gtk_window_present (GTK_WINDOW (editor->priv->link_dialog)); +} + +static void +action_insert_rule_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->hrule_dialog == NULL) + editor->priv->hrule_dialog = + e_html_editor_hrule_dialog_new (editor); + + gtk_window_present (GTK_WINDOW (editor->priv->hrule_dialog)); +} + +static void +action_insert_table_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->table_dialog == NULL) + editor->priv->table_dialog = + e_html_editor_table_dialog_new (editor); + + gtk_window_present (GTK_WINDOW (editor->priv->table_dialog)); +} + +static void +action_insert_text_file_cb (GtkAction *action, + EHTMLEditor *editor) +{ + GtkWidget *dialog; + GtkFileFilter *filter; + + dialog = gtk_file_chooser_dialog_new ( + _("Insert text file"), NULL, + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("Text file")); + gtk_file_filter_add_mime_type (filter, "text/plain"); + gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { + GFile *file; + + file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); + + /* XXX Need a way to cancel this. */ + g_file_load_contents_async ( + file, NULL, (GAsyncReadyCallback) + insert_text_file_ready_cb, + g_object_ref (editor)); + + g_object_unref (file); + } + + gtk_widget_destroy (dialog); +} + +static void +action_language_cb (GtkToggleAction *toggle_action, + EHTMLEditor *editor) +{ + ESpellChecker *checker; + EHTMLEditorView *view; + const gchar *language_code; + GtkAction *add_action; + gchar *action_name; + gboolean active; + + view = e_html_editor_get_view (editor); + checker = e_html_editor_view_get_spell_checker (view); + language_code = gtk_action_get_name (GTK_ACTION (toggle_action)); + + active = gtk_toggle_action_get_active (toggle_action); + e_spell_checker_set_language_active (checker, language_code, active); + + /* Update "Add Word To" context menu item visibility. */ + action_name = g_strdup_printf ("context-spell-add-%s", language_code); + add_action = e_html_editor_get_action (editor, action_name); + gtk_action_set_visible (add_action, active); + g_free (action_name); + + editor_update_static_spell_actions (editor); + + g_signal_emit_by_name (editor, "spell-languages-changed"); +} + +static gboolean +update_mode_combobox (gpointer data) +{ + EHTMLEditor *editor = data; + EHTMLEditorView *view; + GtkAction *action; + gboolean is_html; + + if (!E_IS_HTML_EDITOR (editor)) + return FALSE; + + view = e_html_editor_get_view (editor); + is_html = e_html_editor_view_get_html_mode (view); + + action = gtk_action_group_get_action ( + editor->priv->core_actions, "mode-html"); + gtk_radio_action_set_current_value ( + GTK_RADIO_ACTION (action), (is_html ? 1 : 0)); + + return FALSE; +} + +static void +action_mode_cb (GtkRadioAction *action, + GtkRadioAction *current, + EHTMLEditor *editor) +{ + GtkActionGroup *action_group; + EHTMLEditorView *view; + GtkWidget *style_combo_box; + gboolean is_html; + + view = e_html_editor_get_view (editor); + is_html = e_html_editor_view_get_html_mode (view); + + /* This must be done from idle callback, because apparently we can change + * current value in callback of current value change */ + g_idle_add (update_mode_combobox, editor); + + action_group = editor->priv->html_actions; + gtk_action_group_set_sensitive (action_group, is_html); + + action_group = editor->priv->html_context_actions; + gtk_action_group_set_visible (action_group, is_html); + + gtk_widget_set_sensitive (editor->priv->color_combo_box, is_html); + + if (is_html) { + gtk_widget_show (editor->priv->html_toolbar); + } else { + gtk_widget_hide (editor->priv->html_toolbar); + } + + /* Certain paragraph styles are HTML-only. */ + gtk_action_set_sensitive (ACTION (STYLE_H1), is_html); + gtk_action_set_visible (ACTION (STYLE_H1), is_html); + gtk_action_set_sensitive (ACTION (STYLE_H2), is_html); + gtk_action_set_visible (ACTION (STYLE_H2), is_html); + gtk_action_set_sensitive (ACTION (STYLE_H3), is_html); + gtk_action_set_visible (ACTION (STYLE_H3), is_html); + gtk_action_set_sensitive (ACTION (STYLE_H4), is_html); + gtk_action_set_visible (ACTION (STYLE_H4), is_html); + gtk_action_set_sensitive (ACTION (STYLE_H5), is_html); + gtk_action_set_visible (ACTION (STYLE_H5), is_html); + gtk_action_set_sensitive (ACTION (STYLE_H6), is_html); + gtk_action_set_visible (ACTION (STYLE_H6), is_html); + gtk_action_set_sensitive (ACTION (STYLE_ADDRESS), is_html); + gtk_action_set_visible (ACTION (STYLE_ADDRESS), is_html); + + /* Hide them from the action combo box as well */ + style_combo_box = e_html_editor_get_style_combo_box (editor); + e_action_combo_box_update_model (E_ACTION_COMBO_BOX (style_combo_box)); +} + +static void +action_paste_cb (GtkAction *action, + EHTMLEditor *editor) +{ + EHTMLEditorView *view = e_html_editor_get_view (editor); + + /* Paste only if WebView has focus */ + if (gtk_widget_has_focus (GTK_WIDGET (view))) { + webkit_web_view_paste_clipboard ( + WEBKIT_WEB_VIEW (view)); + + e_html_editor_view_force_spell_check (view); + } +} + +static void +action_paste_quote_cb (GtkAction *action, + EHTMLEditor *editor) +{ + e_html_editor_view_paste_clipboard_quoted ( + e_html_editor_get_view (editor)); + + e_html_editor_view_force_spell_check ( + e_html_editor_get_view (editor)); +} + +static void +action_properties_cell_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->cell_dialog == NULL) { + editor->priv->cell_dialog = + e_html_editor_cell_dialog_new (editor); + } + + e_html_editor_cell_dialog_show ( + E_HTML_EDITOR_CELL_DIALOG (editor->priv->cell_dialog), + editor->priv->table_cell); +} + +static void +action_properties_image_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->image_dialog == NULL) { + editor->priv->image_dialog = + e_html_editor_image_dialog_new (editor); + } + + e_html_editor_image_dialog_show ( + E_HTML_EDITOR_IMAGE_DIALOG (editor->priv->image_dialog), + editor->priv->image); +} + +static void +action_properties_link_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->link_dialog == NULL) { + editor->priv->link_dialog = + e_html_editor_link_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->link_dialog)); +} + +static void +action_properties_page_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->page_dialog == NULL) { + editor->priv->page_dialog = + e_html_editor_page_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->page_dialog)); +} + +static void +action_properties_paragraph_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->paragraph_dialog == NULL) { + editor->priv->paragraph_dialog = + e_html_editor_paragraph_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->paragraph_dialog)); +} + +static void +action_properties_rule_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->hrule_dialog == NULL) { + editor->priv->hrule_dialog = + e_html_editor_hrule_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->hrule_dialog)); +} + +static void +action_properties_table_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->table_dialog == NULL) { + editor->priv->table_dialog = + e_html_editor_table_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->table_dialog)); +} + +static void +action_properties_text_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->text_dialog == NULL) { + editor->priv->text_dialog = + e_html_editor_text_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->text_dialog)); +} + +static void +action_redo_cb (GtkAction *action, + EHTMLEditor *editor) +{ + webkit_web_view_redo ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); +} + +static void +action_select_all_cb (GtkAction *action, + EHTMLEditor *editor) +{ + webkit_web_view_select_all ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); +} + +static void +action_show_find_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->find_dialog == NULL) { + editor->priv->find_dialog = e_html_editor_find_dialog_new (editor); + gtk_action_set_sensitive (ACTION (FIND_AGAIN), TRUE); + } + + gtk_window_present (GTK_WINDOW (editor->priv->find_dialog)); +} + +static void +action_find_again_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->find_dialog == NULL) { + return; + } + + e_html_editor_find_dialog_find_next ( + E_HTML_EDITOR_FIND_DIALOG (editor->priv->find_dialog)); +} + +static void +action_show_replace_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->replace_dialog == NULL) { + editor->priv->replace_dialog = + e_html_editor_replace_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->replace_dialog)); +} + +static void +action_spell_check_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (editor->priv->spell_check_dialog == NULL) { + editor->priv->spell_check_dialog = + e_html_editor_spell_check_dialog_new (editor); + } + + gtk_window_present (GTK_WINDOW (editor->priv->spell_check_dialog)); +} + +static void +action_undo_cb (GtkAction *action, + EHTMLEditor *editor) +{ + webkit_web_view_undo ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); +} + +static void +action_unindent_cb (GtkAction *action, + EHTMLEditor *editor) +{ + e_html_editor_selection_unindent (editor->priv->selection); +} + +static void +action_wrap_lines_cb (GtkAction *action, + EHTMLEditor *editor) +{ + e_html_editor_selection_wrap_lines (editor->priv->selection); +} + +static void +action_show_webkit_inspector_cb (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitWebInspector *inspector; + EHTMLEditorView *view; + + view = e_html_editor_get_view (editor); + inspector = webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (view)); + + webkit_web_inspector_show (inspector); +} + +/***************************************************************************** + * Core Actions + * + * These actions are always enabled. + *****************************************************************************/ + +static GtkActionEntry core_entries[] = { + + { "copy", + GTK_STOCK_COPY, + N_("_Copy"), + "<Control>c", + N_("Copy selected text to the clipboard"), + G_CALLBACK (action_copy_cb) }, + + { "cut", + GTK_STOCK_CUT, + N_("Cu_t"), + "<Control>x", + N_("Cut selected text to the clipboard"), + G_CALLBACK (action_cut_cb) }, + + { "indent", + GTK_STOCK_INDENT, + N_("_Increase Indent"), + "<Control>bracketright", + N_("Increase Indent"), + G_CALLBACK (action_indent_cb) }, + + { "insert-html-file", + NULL, + N_("_HTML File..."), + NULL, + NULL, + G_CALLBACK (action_insert_html_file_cb) }, + + { "insert-text-file", + NULL, + N_("Te_xt File..."), + NULL, + NULL, + G_CALLBACK (action_insert_text_file_cb) }, + + { "paste", + GTK_STOCK_PASTE, + N_("_Paste"), + "<Control>v", + N_("Paste text from the clipboard"), + G_CALLBACK (action_paste_cb) }, + + { "paste-quote", + NULL, + N_("Paste _Quotation"), + "<Shift><Control>v", + NULL, + G_CALLBACK (action_paste_quote_cb) }, + + { "redo", + GTK_STOCK_REDO, + N_("_Redo"), + "<Shift><Control>z", + N_("Redo the last undone action"), + G_CALLBACK (action_redo_cb) }, + + { "select-all", + GTK_STOCK_SELECT_ALL, + N_("Select _All"), + "<Control>a", + NULL, + G_CALLBACK (action_select_all_cb) }, + + { "show-find", + GTK_STOCK_FIND, + N_("_Find..."), + "<Control>f", + N_("Search for text"), + G_CALLBACK (action_show_find_cb) }, + + { "find-again", + NULL, + N_("Find A_gain"), + "<Control>g", + NULL, + G_CALLBACK (action_find_again_cb) }, + + { "show-replace", + GTK_STOCK_FIND_AND_REPLACE, + N_("Re_place..."), + "<Control>h", + N_("Search for and replace text"), + G_CALLBACK (action_show_replace_cb) }, + + { "spell-check", + GTK_STOCK_SPELL_CHECK, + N_("Check _Spelling..."), + "F7", + NULL, + G_CALLBACK (action_spell_check_cb) }, + + { "undo", + GTK_STOCK_UNDO, + N_("_Undo"), + "<Control>z", + N_("Undo the last action"), + G_CALLBACK (action_undo_cb) }, + + { "unindent", + GTK_STOCK_UNINDENT, + N_("_Decrease Indent"), + "<Control>bracketleft", + N_("Decrease Indent"), + G_CALLBACK (action_unindent_cb) }, + + { "wrap-lines", + NULL, + N_("_Wrap Lines"), + "<Control>k", + NULL, + G_CALLBACK (action_wrap_lines_cb) }, + + { "webkit-inspector", + NULL, + N_("Open Inspector"), + NULL, + NULL, + G_CALLBACK (action_show_webkit_inspector_cb) }, + + /* Menus */ + + { "edit-menu", + NULL, + N_("_Edit"), + NULL, + NULL, + NULL }, + + { "file-menu", + NULL, + N_("_File"), + NULL, + NULL, + NULL }, + + { "format-menu", + NULL, + N_("For_mat"), + NULL, + NULL, + NULL }, + + { "paragraph-style-menu", + NULL, + N_("_Paragraph Style"), + NULL, + NULL, + NULL }, + + { "insert-menu", + NULL, + N_("_Insert"), + NULL, + NULL, + NULL }, + + { "justify-menu", + NULL, + N_("_Alignment"), + NULL, + NULL, + NULL }, + + { "language-menu", + NULL, + N_("Current _Languages"), + NULL, + NULL, + NULL }, + + { "view-menu", + NULL, + N_("_View"), + NULL, + NULL, + NULL } +}; + +static GtkRadioActionEntry core_justify_entries[] = { + + { "justify-center", + GTK_STOCK_JUSTIFY_CENTER, + N_("_Center"), + "<Control>e", + N_("Center Alignment"), + E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER }, + + { "justify-left", + GTK_STOCK_JUSTIFY_LEFT, + N_("_Left"), + "<Control>l", + N_("Left Alignment"), + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT }, + + { "justify-right", + GTK_STOCK_JUSTIFY_RIGHT, + N_("_Right"), + "<Control>r", + N_("Right Alignment"), + E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT } +}; + +static GtkRadioActionEntry core_mode_entries[] = { + + { "mode-html", + NULL, + N_("_HTML"), + NULL, + N_("HTML editing mode"), + TRUE }, /* e_html_editor_view_set_html_mode */ + + { "mode-plain", + NULL, + N_("Plain _Text"), + NULL, + N_("Plain text editing mode"), + FALSE } /* e_html_editor_view_set_html_mode */ +}; + +static GtkRadioActionEntry core_style_entries[] = { + + { "style-normal", + NULL, + N_("_Normal"), + "<Control>0", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH }, + + { "style-h1", + NULL, + N_("Header _1"), + "<Control>1", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H1 }, + + { "style-h2", + NULL, + N_("Header _2"), + "<Control>2", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H2 }, + + { "style-h3", + NULL, + N_("Header _3"), + "<Control>3", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H3 }, + + { "style-h4", + NULL, + N_("Header _4"), + "<Control>4", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H4 }, + + { "style-h5", + NULL, + N_("Header _5"), + "<Control>5", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H5 }, + + { "style-h6", + NULL, + N_("Header _6"), + "<Control>6", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H6 }, + + { "style-preformat", + NULL, + N_("_Preformatted"), + "<Control>7", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PRE }, + + { "style-address", + NULL, + N_("A_ddress"), + "<Control>8", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ADDRESS }, + + { "style-blockquote", + NULL, + N_("_Blockquote"), + "<Control>9", + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_BLOCKQUOTE }, + + { "style-list-bullet", + NULL, + N_("_Bulleted List"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST }, + + { "style-list-roman", + NULL, + N_("_Roman Numeral List"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN }, + + { "style-list-number", + NULL, + N_("Numbered _List"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST }, + + { "style-list-alpha", + NULL, + N_("_Alphabetical List"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA } +}; + +/***************************************************************************** + * Core Actions (HTML only) + * + * These actions are only enabled in HTML mode. + *****************************************************************************/ + +static GtkActionEntry html_entries[] = { + + { "insert-image", + "insert-image", + N_("_Image..."), + NULL, + N_("Insert Image"), + G_CALLBACK (action_insert_image_cb) }, + + { "insert-link", + "insert-link", + N_("_Link..."), + NULL, + N_("Insert Link"), + G_CALLBACK (action_insert_link_cb) }, + + { "insert-rule", + "stock_insert-rule", + /* Translators: 'Rule' here means a horizontal line in an HTML text */ + N_("_Rule..."), + NULL, + /* Translators: 'Rule' here means a horizontal line in an HTML text */ + N_("Insert Rule"), + G_CALLBACK (action_insert_rule_cb) }, + + { "insert-table", + "stock_insert-table", + N_("_Table..."), + NULL, + N_("Insert Table"), + G_CALLBACK (action_insert_table_cb) }, + + { "properties-cell", + NULL, + N_("_Cell..."), + NULL, + NULL, + G_CALLBACK (action_properties_cell_cb) }, + + { "properties-image", + NULL, + N_("_Image..."), + NULL, + NULL, + G_CALLBACK (action_properties_image_cb) }, + + { "properties-link", + NULL, + N_("_Link..."), + NULL, + NULL, + G_CALLBACK (action_properties_link_cb) }, + + { "properties-page", + NULL, + N_("Pa_ge..."), + NULL, + NULL, + G_CALLBACK (action_properties_page_cb) }, + + { "properties-rule", + NULL, + /* Translators: 'Rule' here means a horizontal line in an HTML text */ + N_("_Rule..."), + NULL, + NULL, + G_CALLBACK (action_properties_rule_cb) }, + + { "properties-table", + NULL, + N_("_Table..."), + NULL, + NULL, + G_CALLBACK (action_properties_table_cb) }, + + /* Menus */ + + { "font-size-menu", + NULL, + N_("Font _Size"), + NULL, + NULL, + NULL }, + + { "font-style-menu", + NULL, + N_("_Font Style"), + NULL, + NULL, + NULL }, +}; + +static GtkToggleActionEntry html_toggle_entries[] = { + + { "bold", + GTK_STOCK_BOLD, + N_("_Bold"), + "<Control>b", + N_("Bold"), + NULL, + FALSE }, + + { "italic", + GTK_STOCK_ITALIC, + N_("_Italic"), + "<Control>i", + N_("Italic"), + NULL, + FALSE }, + + { "monospaced", + "stock_text-monospaced", + N_("_Plain Text"), + "<Control>t", + N_("Plain Text"), + NULL, + FALSE }, + + { "strikethrough", + GTK_STOCK_STRIKETHROUGH, + N_("_Strikethrough"), + NULL, + N_("Strikethrough"), + NULL, + FALSE }, + + { "underline", + GTK_STOCK_UNDERLINE, + N_("_Underline"), + "<Control>u", + N_("Underline"), + NULL, + FALSE } +}; + +static GtkRadioActionEntry html_size_entries[] = { + + { "size-minus-two", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("-2"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_TINY }, + + { "size-minus-one", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("-1"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_SMALL }, + + { "size-plus-zero", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("+0"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL }, + + { "size-plus-one", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("+1"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_BIG }, + + { "size-plus-two", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("+2"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_BIGGER }, + + { "size-plus-three", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("+3"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_LARGE }, + + { "size-plus-four", + NULL, + /* Translators: This is a font size level. It is shown on a tool bar. Please keep it as short as possible. */ + N_("+4"), + NULL, + NULL, + E_HTML_EDITOR_SELECTION_FONT_SIZE_VERY_LARGE } +}; + +/***************************************************************************** + * Context Menu Actions + * + * These require separate action entries so we can toggle their visiblity + * rather than their sensitivity as we do with main menu / toolbar actions. + * Note that some of these actions use the same callback function as their + * non-context sensitive counterparts. + *****************************************************************************/ + +static GtkActionEntry context_entries[] = { + + { "context-delete-cell", + NULL, + N_("Cell Contents"), + NULL, + NULL, + G_CALLBACK (action_context_delete_cell_cb) }, + + { "context-delete-column", + NULL, + N_("Column"), + NULL, + NULL, + G_CALLBACK (action_context_delete_column_cb) }, + + { "context-delete-row", + NULL, + N_("Row"), + NULL, + NULL, + G_CALLBACK (action_context_delete_row_cb) }, + + { "context-delete-table", + NULL, + N_("Table"), + NULL, + NULL, + G_CALLBACK (action_context_delete_table_cb) }, + + /* Menus */ + + { "context-delete-table-menu", + NULL, + /* Translators: Popup menu item caption, containing all the Delete options for a table */ + N_("Table Delete"), + NULL, + NULL, + NULL }, + + { "context-input-methods-menu", + NULL, + N_("Input Methods"), + NULL, + NULL, + NULL }, + + { "context-insert-table-menu", + NULL, + /* Translators: Popup menu item caption, containing all the Insert options for a table */ + N_("Table Insert"), + NULL, + NULL, + NULL }, + + { "context-properties-menu", + NULL, + N_("Properties"), + NULL, + NULL, + NULL }, +}; + +/***************************************************************************** + * Context Menu Actions (HTML only) + * + * These actions are never visible in plain-text mode. Note that some + * of them use the same callback function as their non-context sensitive + * counterparts. + *****************************************************************************/ + +static GtkActionEntry html_context_entries[] = { + + { "context-insert-column-after", + NULL, + N_("Column After"), + NULL, + NULL, + G_CALLBACK (action_context_insert_column_after_cb) }, + + { "context-insert-column-before", + NULL, + N_("Column Before"), + NULL, + NULL, + G_CALLBACK (action_context_insert_column_before_cb) }, + + { "context-insert-link", + NULL, + N_("Insert _Link"), + NULL, + NULL, + G_CALLBACK (action_insert_link_cb) }, + + { "context-insert-row-above", + NULL, + N_("Row Above"), + NULL, + NULL, + G_CALLBACK (action_context_insert_row_above_cb) }, + + { "context-insert-row-below", + NULL, + N_("Row Below"), + NULL, + NULL, + G_CALLBACK (action_context_insert_row_below_cb) }, + + { "context-insert-table", + NULL, + N_("Table"), + NULL, + NULL, + G_CALLBACK (action_insert_table_cb) }, + + { "context-properties-cell", + NULL, + N_("Cell..."), + NULL, + NULL, + G_CALLBACK (action_properties_cell_cb) }, + + { "context-properties-image", + NULL, + N_("Image..."), + NULL, + NULL, + G_CALLBACK (action_properties_image_cb) }, + + { "context-properties-link", + NULL, + N_("Link..."), + NULL, + NULL, + G_CALLBACK (action_properties_link_cb) }, + + { "context-properties-page", + NULL, + N_("Page..."), + NULL, + NULL, + G_CALLBACK (action_properties_page_cb) }, + + { "context-properties-paragraph", + NULL, + N_("Paragraph..."), + NULL, + NULL, + G_CALLBACK (action_properties_paragraph_cb) }, + + { "context-properties-rule", + NULL, + /* Translators: 'Rule' here means a horizontal line in an HTML text */ + N_("Rule..."), + NULL, + NULL, + G_CALLBACK (action_properties_rule_cb) }, + + { "context-properties-table", + NULL, + N_("Table..."), + NULL, + NULL, + G_CALLBACK (action_properties_table_cb) }, + + { "context-properties-text", + NULL, + N_("Text..."), + NULL, + NULL, + G_CALLBACK (action_properties_text_cb) }, + + { "context-remove-link", + NULL, + N_("Remove Link"), + NULL, + NULL, + G_CALLBACK (action_context_remove_link_cb) } +}; + +/***************************************************************************** + * Context Menu Actions (spell checking only) + * + * These actions are only visible when the word underneath the cursor is + * misspelled. + *****************************************************************************/ + +static GtkActionEntry spell_context_entries[] = { + + { "context-spell-add", + NULL, + N_("Add Word to Dictionary"), + NULL, + NULL, + G_CALLBACK (action_context_spell_add_cb) }, + + { "context-spell-ignore", + NULL, + N_("Ignore Misspelled Word"), + NULL, + NULL, + G_CALLBACK (action_context_spell_ignore_cb) }, + + { "context-spell-add-menu", + NULL, + N_("Add Word To"), + NULL, + NULL, + NULL }, + + /* Menus */ + + { "context-more-suggestions-menu", + NULL, + N_("More Suggestions"), + NULL, + NULL, + NULL } +}; + +static void +editor_actions_setup_languages_menu (EHTMLEditor *editor) +{ + ESpellChecker *checker; + EHTMLEditorView *view; + GtkUIManager *manager; + GtkActionGroup *action_group; + GList *list, *link; + guint merge_id; + + manager = editor->priv->manager; + action_group = editor->priv->language_actions; + view = e_html_editor_get_view (editor); + checker = e_html_editor_view_get_spell_checker (view); + merge_id = gtk_ui_manager_new_merge_id (manager); + + list = e_spell_checker_list_available_dicts (checker); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary = link->data; + GtkToggleAction *action; + gboolean active; + + action = gtk_toggle_action_new ( + e_spell_dictionary_get_code (dictionary), + e_spell_dictionary_get_name (dictionary), + NULL, NULL); + + /* Do this BEFORE connecting to the "toggled" signal. + * We're not prepared to invoke the signal handler yet. + * The "Add Word To" actions have not yet been added. */ + active = e_spell_checker_get_language_active ( + checker, e_spell_dictionary_get_code (dictionary)); + gtk_toggle_action_set_active (action, active); + + g_signal_connect ( + action, "toggled", + G_CALLBACK (action_language_cb), editor); + + gtk_action_group_add_action ( + action_group, GTK_ACTION (action)); + + g_object_unref (action); + + gtk_ui_manager_add_ui ( + manager, merge_id, + "/main-menu/edit-menu/language-menu", + e_spell_dictionary_get_code (dictionary), + e_spell_dictionary_get_code (dictionary), + GTK_UI_MANAGER_AUTO, FALSE); + } + + g_list_free (list); +} + +static void +editor_actions_setup_spell_check_menu (EHTMLEditor *editor) +{ + ESpellChecker *checker; + GtkUIManager *manager; + GtkActionGroup *action_group; + GList *available_dicts, *iter; + guint merge_id; + + manager = editor->priv->manager; + action_group = editor->priv->spell_check_actions;; + checker = e_html_editor_view_get_spell_checker (editor->priv->html_editor_view); + available_dicts = e_spell_checker_list_available_dicts (checker); + merge_id = gtk_ui_manager_new_merge_id (manager); + + for (iter = available_dicts; iter; iter = iter->next) { + ESpellDictionary *dictionary = iter->data; + GtkAction *action; + const gchar *code; + const gchar *name; + gchar *action_label; + gchar *action_name; + + code = e_spell_dictionary_get_code (dictionary); + name = e_spell_dictionary_get_name (dictionary); + + /* Add a suggestion menu. */ + action_name = g_strdup_printf ( + "context-spell-suggest-%s-menu", code); + + action = gtk_action_new (action_name, name, NULL, NULL); + gtk_action_group_add_action (action_group, action); + g_object_unref (action); + + gtk_ui_manager_add_ui ( + manager, merge_id, + "/context-menu/context-spell-suggest", + action_name, action_name, + GTK_UI_MANAGER_MENU, FALSE); + + g_free (action_name); + + /* Add an item to the "Add Word To" menu. */ + action_name = g_strdup_printf ("context-spell-add-%s", code); + /* Translators: %s will be replaced with the actual dictionary + * name, where a user can add a word to. This is part of an + * "Add Word To" submenu. */ + action_label = g_strdup_printf (_("%s Dictionary"), name); + + action = gtk_action_new ( + action_name, action_label, NULL, NULL); + + g_signal_connect ( + action, "activate", + G_CALLBACK (action_context_spell_add_cb), editor); + + /* Visibility is dependent on whether the + * corresponding language action is active. */ + gtk_action_set_visible (action, FALSE); + + gtk_action_group_add_action (action_group, action); + + g_object_unref (action); + + gtk_ui_manager_add_ui ( + manager, merge_id, + "/context-menu/context-spell-add-menu", + action_name, action_name, + GTK_UI_MANAGER_AUTO, FALSE); + + g_free (action_label); + g_free (action_name); + } + + g_list_free (available_dicts); +} + +void +editor_actions_init (EHTMLEditor *editor) +{ + GtkAction *action; + GtkActionGroup *action_group; + GtkUIManager *manager; + const gchar *domain; + EHTMLEditorView *view; + GSettings *settings; + + g_return_if_fail (E_IS_HTML_EDITOR (editor)); + + manager = e_html_editor_get_ui_manager (editor); + domain = GETTEXT_PACKAGE; + view = e_html_editor_get_view (editor); + + /* Core Actions */ + action_group = editor->priv->core_actions; + gtk_action_group_set_translation_domain (action_group, domain); + gtk_action_group_add_actions ( + action_group, core_entries, + G_N_ELEMENTS (core_entries), editor); + gtk_action_group_add_radio_actions ( + action_group, core_justify_entries, + G_N_ELEMENTS (core_justify_entries), + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT, + NULL, NULL); + gtk_action_group_add_radio_actions ( + action_group, core_mode_entries, + G_N_ELEMENTS (core_mode_entries), + TRUE, + G_CALLBACK (action_mode_cb), editor); + gtk_action_group_add_radio_actions ( + action_group, core_style_entries, + G_N_ELEMENTS (core_style_entries), + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH, + NULL, NULL); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + action = gtk_action_group_get_action (action_group, "mode-html"); + g_object_bind_property ( + view, "html-mode", + action, "current-value", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + /* Synchronize wiget mode with the buttons */ + e_html_editor_view_set_html_mode (view, TRUE); + + /* Face Action */ + action = e_emoticon_action_new ( + "insert-emoticon", _("_Emoticon"), + _("Insert Emoticon"), NULL); + g_object_set (action, "icon-name", "face-smile", NULL); + g_signal_connect ( + action, "item-activated", + G_CALLBACK (action_insert_emoticon_cb), editor); + gtk_action_group_add_action (action_group, action); + g_object_unref (action); + + /* Core Actions (HTML only) */ + action_group = editor->priv->html_actions; + gtk_action_group_set_translation_domain (action_group, domain); + gtk_action_group_add_actions ( + action_group, html_entries, + G_N_ELEMENTS (html_entries), editor); + gtk_action_group_add_toggle_actions ( + action_group, html_toggle_entries, + G_N_ELEMENTS (html_toggle_entries), editor); + gtk_action_group_add_radio_actions ( + action_group, html_size_entries, + G_N_ELEMENTS (html_size_entries), + E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL, + NULL, NULL); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Context Menu Actions */ + action_group = editor->priv->context_actions; + gtk_action_group_set_translation_domain (action_group, domain); + gtk_action_group_add_actions ( + action_group, context_entries, + G_N_ELEMENTS (context_entries), editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Context Menu Actions (HTML only) */ + action_group = editor->priv->html_context_actions; + gtk_action_group_set_translation_domain (action_group, domain); + gtk_action_group_add_actions ( + action_group, html_context_entries, + G_N_ELEMENTS (html_context_entries), editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Context Menu Actions (spell check only) */ + action_group = editor->priv->spell_check_actions; + gtk_action_group_set_translation_domain (action_group, domain); + gtk_action_group_add_actions ( + action_group, spell_context_entries, + G_N_ELEMENTS (spell_context_entries), editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Language actions are generated dynamically. */ + editor_actions_setup_languages_menu (editor); + action_group = editor->priv->language_actions; + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Some spell check actions are generated dynamically. */ + action_group = editor->priv->suggestion_actions; + editor_actions_setup_spell_check_menu (editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + /* Do this after all language actions are initialized. */ + editor_update_static_spell_actions (editor); + + /* Fine Tuning */ + + g_object_set ( + G_OBJECT (ACTION (SHOW_FIND)), + "short-label", _("_Find"), NULL); + g_object_set ( + G_OBJECT (ACTION (SHOW_REPLACE)), + "short-label", _("Re_place"), NULL); + g_object_set ( + G_OBJECT (ACTION (INSERT_IMAGE)), + "short-label", _("_Image"), NULL); + g_object_set ( + G_OBJECT (ACTION (INSERT_LINK)), + "short-label", _("_Link"), NULL); + g_object_set ( + G_OBJECT (ACTION (INSERT_RULE)), + /* Translators: 'Rule' here means a horizontal line in an HTML text */ + "short-label", _("_Rule"), NULL); + g_object_set ( + G_OBJECT (ACTION (INSERT_TABLE)), + "short-label", _("_Table"), NULL); + + gtk_action_set_sensitive (ACTION (UNINDENT), FALSE); + gtk_action_set_sensitive (ACTION (FIND_AGAIN), FALSE); + gtk_action_set_sensitive (ACTION (SPELL_CHECK), FALSE); + + g_object_bind_property ( + view, "can-redo", + ACTION (REDO), "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "can-undo", + ACTION (UNDO), "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "can-copy", + ACTION (COPY), "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "can-cut", + ACTION (CUT), "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "can-paste", + ACTION (PASTE), "sensitive", + G_BINDING_SYNC_CREATE); + + /* This is connected to JUSTIFY_LEFT action only, but + * it automatically applies on all actions in the group. */ + g_object_bind_property ( + editor->priv->selection, "alignment", + ACTION (JUSTIFY_LEFT), "current-value", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "bold", + ACTION (BOLD), "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "font-size", + ACTION (FONT_SIZE_GROUP), "current-value", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "block-format", + ACTION (STYLE_NORMAL), "current-value", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "indented", + ACTION (UNINDENT), "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + editor->priv->selection, "italic", + ACTION (ITALIC), "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "monospaced", + ACTION (MONOSPACED), "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "strikethrough", + ACTION (STRIKETHROUGH), "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + g_object_bind_property ( + editor->priv->selection, "underline", + ACTION (UNDERLINE), "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + + /* Disable all actions and toolbars when editor is not editable */ + g_object_bind_property ( + view, "editable", + editor->priv->core_actions, "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "editable", + editor->priv->html_actions, "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "editable", + editor->priv->spell_check_actions, "sensitive", + G_BINDING_SYNC_CREATE); + g_object_bind_property ( + view, "editable", + editor->priv->suggestion_actions, "sensitive", + G_BINDING_SYNC_CREATE); + + settings = g_settings_new ("org.gnome.evolution.mail"); + gtk_action_set_visible ( + ACTION (WEBKIT_INSPECTOR), + g_settings_get_boolean (settings, "composer-developer-mode")); + g_object_unref (settings); +} diff --git a/e-util/e-html-editor-actions.h b/e-util/e-html-editor-actions.h new file mode 100644 index 0000000000..8999add9dc --- /dev/null +++ b/e-util/e-html-editor-actions.h @@ -0,0 +1,155 @@ +/* e-html-editor-actions.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_ACTIONS_H +#define E_HTML_EDITOR_ACTIONS_H + +#define E_HTML_EDITOR_ACTION(editor, name) \ + (e_html_editor_get_action (E_HTML_EDITOR (editor), (name))) + +#define E_HTML_EDITOR_ACTION_BOLD(editor) \ + E_HTML_EDITOR_ACTION ((editor), "bold") +#define E_HTML_EDITOR_ACTION_CONTEXT_DELETE_CELL(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-delete-cell") +#define E_HTML_EDITOR_ACTION_CONTEXT_DELETE_COLUMN(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-delete-column") +#define E_HTML_EDITOR_ACTION_CONTEXT_DELETE_ROW(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-delete-row") +#define E_HTML_EDITOR_ACTION_CONTEXT_DELETE_TABLE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-delete-table") +#define E_HTML_EDITOR_ACTION_CONTEXT_INSERT_COLUMN_AFTER(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-insert-column-after") +#define E_HTML_EDITOR_ACTION_CONTEXT_INSERT_COLUMN_BEFORE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-insert-column-before") +#define E_HTML_EDITOR_ACTION_CONTEXT_INSERT_ROW_ABOVE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-insert-row-above") +#define E_HTML_EDITOR_ACTION_CONTEXT_INSERT_ROW_BELOW(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-insert-row-below") +#define E_HTML_EDITOR_ACTION_CONTEXT_INSERT_TABLE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-insert-table") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_CELL(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-cell") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_IMAGE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-image") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_LINK(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-link") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_PARAGRAPH(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-paragraph") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_RULE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-rule") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_TABLE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-table") +#define E_HTML_EDITOR_ACTION_CONTEXT_PROPERTIES_TEXT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-properties-text") +#define E_HTML_EDITOR_ACTION_CONTEXT_REMOVE_LINK(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-remove-link") +#define E_HTML_EDITOR_ACTION_CONTEXT_SPELL_ADD(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-spell-add") +#define E_HTML_EDITOR_ACTION_CONTEXT_SPELL_ADD_MENU(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-spell-add-menu") +#define E_HTML_EDITOR_ACTION_CONTEXT_SPELL_IGNORE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "context-spell-ignore") +#define E_HTML_EDITOR_ACTION_COPY(editor) \ + E_HTML_EDITOR_ACTION ((editor), "copy") +#define E_HTML_EDITOR_ACTION_CUT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "cut") +#define E_HTML_EDITOR_ACTION_EDIT_MENU(editor) \ + E_HTML_EDITOR_ACTION ((editor), "edit-menu") +#define E_HTML_EDITOR_ACTION_FIND_AGAIN(editor) \ + E_HTML_EDITOR_ACTION ((editor), "find-again") +#define E_HTML_EDITOR_ACTION_FONT_SIZE_GROUP(editor) \ + E_HTML_EDITOR_ACTION ((editor), "size-plus-zero") +#define E_HTML_EDITOR_ACTION_FORMAT_MENU(editor) \ + E_HTML_EDITOR_ACTION ((editor), "format-menu") +#define E_HTML_EDITOR_ACTION_FORMAT_TEXT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "format-text") +#define E_HTML_EDITOR_ACTION_INSERT_IMAGE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-image") +#define E_HTML_EDITOR_ACTION_INSERT_LINK(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-link") +#define E_HTML_EDITOR_ACTION_INSERT_MENU(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-menu") +#define E_HTML_EDITOR_ACTION_INSERT_RULE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-rule") +#define E_HTML_EDITOR_ACTION_INSERT_TABLE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-table") +#define E_HTML_EDITOR_ACTION_ITALIC(editor) \ + E_HTML_EDITOR_ACTION ((editor), "italic") +#define E_HTML_EDITOR_ACTION_JUSTIFY_CENTER(editor) \ + E_HTML_EDITOR_ACTION ((editor), "justify-center") +#define E_HTML_EDITOR_ACTION_JUSTIFY_LEFT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "justify-left") +#define E_HTML_EDITOR_ACTION_JUSTIFY_RIGHT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "justify-right") +#define E_HTML_EDITOR_ACTION_MODE_HTML(editor) \ + E_HTML_EDITOR_ACTION ((editor), "mode-html") +#define E_HTML_EDITOR_ACTION_MODE_PLAIN(editor) \ + E_HTML_EDITOR_ACTION ((editor), "mode-plain") +#define E_HTML_EDITOR_ACTION_MONOSPACED(editor) \ + E_HTML_EDITOR_ACTION ((editor), "monospaced") +#define E_HTML_EDITOR_ACTION_PASTE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "paste") +#define E_HTML_EDITOR_ACTION_PROPERTIES_RULE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "properties-rule") +#define E_HTML_EDITOR_ACTION_PROPERTIES_TABLE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "properties-table") +#define E_HTML_EDITOR_ACTION_REDO(editor) \ + E_HTML_EDITOR_ACTION ((editor), "redo") +#define E_HTML_EDITOR_ACTION_SHOW_FIND(editor) \ + E_HTML_EDITOR_ACTION ((editor), "show-find") +#define E_HTML_EDITOR_ACTION_SHOW_REPLACE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "show-replace") +#define E_HTML_EDITOR_ACTION_SIZE_PLUS_ZERO(editor) \ + E_HTML_EDITOR_ACTION ((editor), "size-plus-zero") +#define E_HTML_EDITOR_ACTION_SPELL_CHECK(editor) \ + E_HTML_EDITOR_ACTION ((editor), "spell-check") +#define E_HTML_EDITOR_ACTION_STRIKETHROUGH(editor) \ + E_HTML_EDITOR_ACTION ((editor), "strikethrough") +#define E_HTML_EDITOR_ACTION_STYLE_ADDRESS(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-address") +#define E_HTML_EDITOR_ACTION_STYLE_H1(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h1") +#define E_HTML_EDITOR_ACTION_STYLE_H2(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h2") +#define E_HTML_EDITOR_ACTION_STYLE_H3(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h3") +#define E_HTML_EDITOR_ACTION_STYLE_H4(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h4") +#define E_HTML_EDITOR_ACTION_STYLE_H5(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h5") +#define E_HTML_EDITOR_ACTION_STYLE_H6(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-h6") +#define E_HTML_EDITOR_ACTION_STYLE_NORMAL(editor) \ + E_HTML_EDITOR_ACTION ((editor), "style-normal") +#define E_HTML_EDITOR_ACTION_TEST_URL(editor) \ + E_HTML_EDITOR_ACTION ((editor), "test-url") +#define E_HTML_EDITOR_ACTION_UNDERLINE(editor) \ + E_HTML_EDITOR_ACTION ((editor), "underline") +#define E_HTML_EDITOR_ACTION_UNDO(editor) \ + E_HTML_EDITOR_ACTION ((editor), "undo") +#define E_HTML_EDITOR_ACTION_UNINDENT(editor) \ + E_HTML_EDITOR_ACTION ((editor), "unindent") +#define E_HTML_EDITOR_ACTION_WEBKIT_INSPECTOR(editor) \ + E_HTML_EDITOR_ACTION ((editor), "webkit-inspector") + +#endif /* E_HTML_EDITOR_ACTIONS_H */ diff --git a/e-util/e-html-editor-cell-dialog.c b/e-util/e-html-editor-cell-dialog.c new file mode 100644 index 0000000000..2b487288ae --- /dev/null +++ b/e-util/e-html-editor-cell-dialog.c @@ -0,0 +1,872 @@ +/* + * e-html-editor-cell-dialog.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-cell-dialog.h" + +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include "e-color-combo.h" +#include "e-html-editor-utils.h" +#include "e-image-chooser-dialog.h" +#include "e-misc-utils.h" + +#define E_HTML_EDITOR_CELL_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_CELL_DIALOG, EHTMLEditorCellDialogPrivate)) + +struct _EHTMLEditorCellDialogPrivate { + GtkWidget *scope_cell_button; + GtkWidget *scope_table_button; + GtkWidget *scope_row_button; + GtkWidget *scope_column_button; + + GtkWidget *halign_combo; + GtkWidget *valign_combo; + + GtkWidget *wrap_text_check; + GtkWidget *header_style_check; + + GtkWidget *width_check; + GtkWidget *width_edit; + GtkWidget *width_units; + + GtkWidget *row_span_edit; + GtkWidget *col_span_edit; + + GtkWidget *background_color_picker; + GtkWidget *background_image_chooser; + + WebKitDOMElement *cell; + guint scope; +}; + +enum { + SCOPE_CELL, + SCOPE_ROW, + SCOPE_COLUMN, + SCOPE_TABLE +} DialogScope; + +static GdkRGBA white = { 1, 1, 1, 1 }; + +typedef void (*DOMStrFunc) (WebKitDOMHTMLTableCellElement *cell, const gchar *val, gpointer user_data); +typedef void (*DOMUlongFunc) (WebKitDOMHTMLTableCellElement *cell, gulong val, gpointer user_data); +typedef void (*DOMBoolFunc) (WebKitDOMHTMLTableCellElement *cell, gboolean val, gpointer user_data); + +G_DEFINE_TYPE ( + EHTMLEditorCellDialog, + e_html_editor_cell_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +call_cell_dom_func (WebKitDOMHTMLTableCellElement *cell, + gpointer func, + GValue *value, + gpointer user_data) +{ + if (G_VALUE_HOLDS_STRING (value)) { + DOMStrFunc f = func; + f (cell, g_value_get_string (value), user_data); + } else if (G_VALUE_HOLDS_ULONG (value)) { + DOMUlongFunc f = func; + f (cell, g_value_get_ulong (value), user_data); + } else if (G_VALUE_HOLDS_BOOLEAN (value)) { + DOMBoolFunc f = func; + f (cell, g_value_get_boolean (value), user_data); + } +} + +static void +for_each_cell_do (WebKitDOMElement *row, + gpointer func, + GValue *value, + gpointer user_data) +{ + WebKitDOMHTMLCollection *cells; + gulong ii, length; + cells = webkit_dom_html_table_row_element_get_cells ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + length = webkit_dom_html_collection_get_length (cells); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *cell; + cell = webkit_dom_html_collection_item (cells, ii); + if (!cell) { + continue; + } + + call_cell_dom_func ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (cell), func, value, user_data); + } +} + +static void +html_editor_cell_dialog_set_attribute (EHTMLEditorCellDialog *dialog, + gpointer func, + GValue *value, + gpointer user_data) +{ + if (dialog->priv->scope == SCOPE_CELL) { + + call_cell_dom_func ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell), + func, value, user_data); + + } else if (dialog->priv->scope == SCOPE_COLUMN) { + gulong index, ii, length; + WebKitDOMElement *table; + WebKitDOMHTMLCollection *rows; + + index = webkit_dom_html_table_cell_element_get_cell_index ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell)); + table = e_html_editor_dom_node_find_parent_element ( + WEBKIT_DOM_NODE (dialog->priv->cell), "TABLE"); + if (!table) { + return; + } + + rows = webkit_dom_html_table_element_get_rows ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table)); + length = webkit_dom_html_collection_get_length (rows); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *row, *cell; + WebKitDOMHTMLCollection *cells; + + row = webkit_dom_html_collection_item (rows, ii); + cells = webkit_dom_html_table_row_element_get_cells ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + cell = webkit_dom_html_collection_item (cells, index); + if (!cell) { + continue; + } + + call_cell_dom_func ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (cell), + func, value, user_data); + } + + } else if (dialog->priv->scope == SCOPE_ROW) { + WebKitDOMElement *row; + + row = e_html_editor_dom_node_find_parent_element ( + WEBKIT_DOM_NODE (dialog->priv->cell), "TR"); + if (!row) { + return; + } + + for_each_cell_do (row, func, value, user_data); + + } else if (dialog->priv->scope == SCOPE_TABLE) { + gulong ii, length; + WebKitDOMElement *table; + WebKitDOMHTMLCollection *rows; + + table = e_html_editor_dom_node_find_parent_element ( + WEBKIT_DOM_NODE (dialog->priv->cell), "TABLE"); + if (!table) { + return; + } + + rows = webkit_dom_html_table_element_get_rows ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table)); + length = webkit_dom_html_collection_get_length (rows); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *row; + + row = webkit_dom_html_collection_item (rows, ii); + if (!row) { + continue; + } + + for_each_cell_do ( + WEBKIT_DOM_ELEMENT (row), func, value, user_data); + } + } +} + +static void +html_editor_cell_dialog_set_scope (EHTMLEditorCellDialog *dialog) +{ + if (gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->scope_cell_button))) { + + dialog->priv->scope = SCOPE_CELL; + + } else if (gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->scope_row_button))) { + + dialog->priv->scope = SCOPE_ROW; + + } else if (gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->scope_column_button))) { + + dialog->priv->scope = SCOPE_COLUMN; + + } else if (gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->scope_table_button))) { + + dialog->priv->scope = SCOPE_TABLE; + + } +} + +static void +html_editor_cell_dialog_set_valign (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_STRING); + g_value_set_string ( + &val, + gtk_combo_box_get_active_id ( + GTK_COMBO_BOX (dialog->priv->valign_combo))); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_v_align, &val, NULL); + + g_value_unset (&val); +} + +static void +html_editor_cell_dialog_set_halign (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_STRING); + g_value_set_string ( + &val, + gtk_combo_box_get_active_id ( + GTK_COMBO_BOX (dialog->priv->halign_combo))); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_align, &val, NULL); + + g_value_unset (&val); +} + +static void +html_editor_cell_dialog_set_wrap_text (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_BOOLEAN); + g_value_set_boolean ( + &val, + !gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->wrap_text_check))); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_no_wrap, &val, NULL); +} + +static void +cell_set_header_style (WebKitDOMHTMLTableCellElement *cell, + gboolean header_style, + gpointer user_data) +{ + EHTMLEditorCellDialog *dialog = user_data; + WebKitDOMDocument *document; + WebKitDOMNodeList *nodes; + WebKitDOMElement *new_cell; + gulong length, ii; + gchar *tagname; + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (cell)); + tagname = webkit_dom_element_get_tag_name (WEBKIT_DOM_ELEMENT (cell)); + + if (header_style && (g_ascii_strncasecmp (tagname, "TD", 2) == 0)) { + + new_cell = webkit_dom_document_create_element (document, "TH", NULL); + + } else if (!header_style && (g_ascii_strncasecmp (tagname, "TH", 2) == 0)) { + + new_cell = webkit_dom_document_create_element (document, "TD", NULL); + + } else { + g_free (tagname); + return; + } + + /* Move all child nodes from cell to new_cell */ + nodes = webkit_dom_node_get_child_nodes (WEBKIT_DOM_NODE (cell)); + length = webkit_dom_node_list_get_length (nodes); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node; + + node = webkit_dom_node_list_item (nodes, ii); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (new_cell), node, NULL); + } + + /* Insert new_cell before cell and remove cell */ + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (cell)), + WEBKIT_DOM_NODE (new_cell), + WEBKIT_DOM_NODE (cell), NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (cell)), + WEBKIT_DOM_NODE (cell), NULL); + + dialog->priv->cell = new_cell; + + g_free (tagname); +} + +static void +html_editor_cell_dialog_set_header_style (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_BOOLEAN); + g_value_set_boolean ( + &val, + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->header_style_check))); + + html_editor_cell_dialog_set_attribute ( + dialog, cell_set_header_style, &val, dialog); +} + +static void +html_editor_cell_dialog_set_width (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + gchar *width; + + if (!gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check))) { + + width = g_strdup ("auto"); + } else { + + width = g_strdup_printf ( + "%d%s", + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->width_edit)), + ((gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->width_units)) == 0) ? + "px" : "%")); + } + + g_value_init (&val, G_TYPE_STRING); + g_value_take_string (&val, width); + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_width, &val, NULL); + + g_free (width); +} + +static void +html_editor_cell_dialog_set_column_span (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_ULONG); + g_value_set_ulong ( + &val, + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->col_span_edit))); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_col_span, &val, NULL); +} + +static void +html_editor_cell_dialog_set_row_span (EHTMLEditorCellDialog *dialog) +{ + GValue val = { 0 }; + + g_value_init (&val, G_TYPE_ULONG); + g_value_set_ulong ( + &val, + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->row_span_edit))); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_row_span, &val, NULL); +} + +static void +html_editor_cell_dialog_set_background_color (EHTMLEditorCellDialog *dialog) +{ + gchar *color; + GdkRGBA rgba; + GValue val = { 0 }; + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), &rgba); + color = g_strdup_printf ("#%06x", e_rgba_to_value (&rgba)); + + g_value_init (&val, G_TYPE_STRING); + g_value_take_string (&val, color); + + html_editor_cell_dialog_set_attribute ( + dialog, webkit_dom_html_table_cell_element_set_bg_color, &val, NULL); + + g_free (color); +} + +static void +cell_set_background_image (WebKitDOMHTMLTableCellElement *cell, + const gchar *uri, + gpointer user_data) +{ + if (!uri || !*uri) { + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (cell), "background"); + } else { + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (cell), "background", uri, NULL); + } +} + +static void +html_editor_cell_dialog_set_background_image (EHTMLEditorCellDialog *dialog) +{ + const gchar *uri; + GValue val = { 0 }; + + uri = gtk_file_chooser_get_uri ( + GTK_FILE_CHOOSER (dialog->priv->background_image_chooser)); + + g_value_init (&val, G_TYPE_STRING); + g_value_take_string (&val, (gchar *) uri); + + html_editor_cell_dialog_set_attribute ( + dialog, cell_set_background_image, &val, NULL); +} + +static void +html_editor_cell_dialog_show (GtkWidget *widget) +{ + EHTMLEditorCellDialog *dialog; + gchar *tmp; + GdkRGBA color; + + dialog = E_HTML_EDITOR_CELL_DIALOG (widget); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->scope_cell_button), TRUE); + + tmp = webkit_dom_html_table_cell_element_get_align ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell)); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->halign_combo), + (tmp && *tmp) ? tmp : "left"); + g_free (tmp); + + tmp = webkit_dom_html_table_cell_element_get_v_align ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell)); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->valign_combo), + (tmp && *tmp) ? tmp : "middle"); + g_free (tmp); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->wrap_text_check), + !webkit_dom_html_table_cell_element_get_no_wrap ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell))); + + tmp = webkit_dom_element_get_tag_name ( + WEBKIT_DOM_ELEMENT (dialog->priv->cell)); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->header_style_check), + (g_ascii_strncasecmp (tmp, "TH", 2) == 0)); + g_free (tmp); + + tmp = webkit_dom_html_table_cell_element_get_width ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell)); + if (tmp && *tmp) { + gint val = atoi (tmp); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), val); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check), TRUE); + } else { + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), 0); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check), FALSE); + } + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->width_units), "units-px"); + g_free (tmp); + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->row_span_edit), + webkit_dom_html_table_cell_element_get_row_span ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell))); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->col_span_edit), + webkit_dom_html_table_cell_element_get_col_span ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell))); + + if (webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->cell), "background")) { + tmp = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->cell), "background"); + + gtk_file_chooser_set_uri ( + GTK_FILE_CHOOSER (dialog->priv->background_image_chooser), + tmp); + + g_free (tmp); + } else { + gtk_file_chooser_unselect_all ( + GTK_FILE_CHOOSER (dialog->priv->background_image_chooser)); + } + + tmp = webkit_dom_html_table_cell_element_get_bg_color ( + WEBKIT_DOM_HTML_TABLE_CELL_ELEMENT (dialog->priv->cell)); + if (!tmp || *tmp) { + color = white; + } + if (gdk_rgba_parse (&color, tmp)) { + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), + &color); + } else { + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), + &white); + } + g_free (tmp); + + GTK_WIDGET_CLASS (e_html_editor_cell_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_cell_dialog_class_init (EHTMLEditorCellDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorCellDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_cell_dialog_show; +} + +static void +e_html_editor_cell_dialog_init (EHTMLEditorCellDialog *dialog) +{ + GtkGrid *main_layout, *grid; + GtkWidget *widget; + GtkFileFilter *file_filter; + + dialog->priv = E_HTML_EDITOR_CELL_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == Scope == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Scope</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Scope: cell */ + widget = gtk_image_new_from_icon_name ("stock_select-cell", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + widget = gtk_radio_button_new_with_mnemonic (NULL, _("C_ell")); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->scope_cell_button = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_scope), dialog); + + /* Scope: row */ + widget = gtk_image_new_from_icon_name ("stock_select-row", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + + widget = gtk_radio_button_new_with_mnemonic_from_widget ( + GTK_RADIO_BUTTON (dialog->priv->scope_cell_button), _("_Row")); + gtk_grid_attach (grid, widget, 3, 0, 1, 1); + dialog->priv->scope_row_button = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_scope), dialog); + + /* Scope: table */ + widget = gtk_image_new_from_icon_name ("stock_select-table", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + widget = gtk_radio_button_new_with_mnemonic_from_widget ( + GTK_RADIO_BUTTON (dialog->priv->scope_cell_button), _("_Table")); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + dialog->priv->scope_table_button = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_scope), dialog); + + /* Scope: column */ + widget = gtk_image_new_from_icon_name ("stock_select-column", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 2, 1, 1, 1); + + widget = gtk_radio_button_new_with_mnemonic_from_widget ( + GTK_RADIO_BUTTON (dialog->priv->scope_cell_button), _("Col_umn")); + gtk_grid_attach (grid, widget, 3, 1, 1, 1); + dialog->priv->scope_column_button = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_scope), dialog); + + /* == Alignment & Behavior == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Alignment & Behavior</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Horizontal */ + widget = gtk_combo_box_text_new (); + gtk_widget_set_hexpand (widget, TRUE); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "left", _("Left")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "center", _("Center")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "right", _("Right")); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->halign_combo = widget; + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_cell_dialog_set_halign), dialog); + + widget = gtk_label_new_with_mnemonic (_("_Horizontal:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->halign_combo); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Vertical */ + widget = gtk_combo_box_text_new (); + gtk_widget_set_hexpand (widget, TRUE); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "top", _("Top")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "middle", _("Middle")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "bottom", _("Bottom")); + gtk_grid_attach (grid, widget, 3, 0, 1, 1); + dialog->priv->valign_combo = widget; + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_cell_dialog_set_valign), dialog); + + widget = gtk_label_new_with_mnemonic (_("_Vertical:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->valign_combo); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + + /* Wrap Text */ + widget = gtk_check_button_new_with_mnemonic (_("_Wrap Text")); + dialog->priv->wrap_text_check = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_wrap_text), dialog); + + /* Header Style */ + widget = gtk_check_button_new_with_mnemonic (_("_Header Style")); + dialog->priv->header_style_check = widget; + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_cell_dialog_set_header_style), dialog); + + widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); + gtk_box_pack_start (GTK_BOX (widget), dialog->priv->wrap_text_check, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (widget), dialog->priv->header_style_check, FALSE, FALSE, 0); + gtk_grid_attach (grid, widget, 0, 1, 4, 1); + + /* == Layout == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Layout</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 4, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 5, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Width */ + widget = gtk_check_button_new_with_mnemonic (_("_Width")); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + dialog->priv->width_check = widget; + + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->width_edit = widget; + + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_cell_dialog_set_width), dialog); + g_object_bind_property ( + dialog->priv->width_check, "active", + widget, "sensitive", + G_BINDING_SYNC_CREATE); + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "unit-px", "px"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "unit-percent", "%"); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + dialog->priv->width_units = widget; + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_cell_dialog_set_width), dialog); + g_object_bind_property ( + dialog->priv->width_check, "active", + widget, "sensitive", + G_BINDING_SYNC_CREATE); + + /* Row Span */ + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 4, 0, 1, 1); + dialog->priv->row_span_edit = widget; + + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_cell_dialog_set_row_span), dialog); + + widget = gtk_label_new_with_mnemonic (_("Row S_pan:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->row_span_edit); + gtk_grid_attach (grid, widget, 3, 0, 1, 1); + + /* Column Span */ + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 4, 1, 1, 1); + dialog->priv->col_span_edit = widget; + + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_cell_dialog_set_column_span), dialog); + + widget = gtk_label_new_with_mnemonic (_("Co_lumn Span:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->col_span_edit); + gtk_grid_attach (grid, widget, 3, 1, 1, 1); + + /* == Background == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Background</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 6, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 7, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Color */ + widget = e_color_combo_new (); + e_color_combo_set_default_color (E_COLOR_COMBO (widget), &white); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_cell_dialog_set_background_color), dialog); + dialog->priv->background_color_picker = widget; + + widget = gtk_label_new_with_mnemonic (_("C_olor:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_color_picker); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Image */ + widget = e_image_chooser_dialog_new ( + _("Choose Background Image"), + GTK_WINDOW (dialog)); + dialog->priv->background_image_chooser = widget; + + file_filter = gtk_file_filter_new (); + gtk_file_filter_set_name (file_filter, _("Images")); + gtk_file_filter_add_mime_type (file_filter, "image/*"); + + widget = gtk_file_chooser_button_new_with_dialog ( + dialog->priv->background_image_chooser); + gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (widget), file_filter); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + g_signal_connect_swapped ( + widget, "file-set", + G_CALLBACK (html_editor_cell_dialog_set_background_image), dialog); + dialog->priv->background_image_chooser = widget; + + widget =gtk_label_new_with_mnemonic (_("_Image:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_image_chooser); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_cell_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_CELL_DIALOG, + "editor", editor, + "title", N_("Cell Properties"), + NULL)); +} + +void +e_html_editor_cell_dialog_show (EHTMLEditorCellDialog *dialog, + WebKitDOMNode *cell) +{ + EHTMLEditorCellDialogClass *class; + + g_return_if_fail (E_IS_HTML_EDITOR_CELL_DIALOG (dialog)); + g_return_if_fail (cell != NULL); + + dialog->priv->cell = e_html_editor_dom_node_find_parent_element (cell, "TD"); + if (dialog->priv->cell == NULL) { + dialog->priv->cell = + e_html_editor_dom_node_find_parent_element (cell, "TH"); + } + + class = E_HTML_EDITOR_CELL_DIALOG_GET_CLASS (dialog); + GTK_WIDGET_CLASS (class)->show (GTK_WIDGET (dialog)); +} + diff --git a/e-util/e-html-editor-cell-dialog.h b/e-util/e-html-editor-cell-dialog.h new file mode 100644 index 0000000000..3a7f60822f --- /dev/null +++ b/e-util/e-html-editor-cell-dialog.h @@ -0,0 +1,72 @@ +/* + * e-html-editor-cell-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_CELL_DIALOG_H +#define E_HTML_EDITOR_CELL_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_CELL_DIALOG \ + (e_html_editor_cell_dialog_get_type ()) +#define E_HTML_EDITOR_CELL_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_CELL_DIALOG, EHTMLEditorCellDialog)) +#define E_HTML_EDITOR_CELL_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_CELL_DIALOG, EHTMLEditorCellDialogClass)) +#define E_IS_HTML_EDITOR_CELL_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_CELL_DIALOG)) +#define E_IS_HTML_EDITOR_CELL_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_CELL_DIALOG)) +#define E_HTML_EDITOR_CELL_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_CELL_DIALOG, EHTMLEditorCellDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorCellDialog EHTMLEditorCellDialog; +typedef struct _EHTMLEditorCellDialogClass EHTMLEditorCellDialogClass; +typedef struct _EHTMLEditorCellDialogPrivate EHTMLEditorCellDialogPrivate; + +struct _EHTMLEditorCellDialog { + EHTMLEditorDialog parent; + EHTMLEditorCellDialogPrivate *priv; +}; + +struct _EHTMLEditorCellDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_cell_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_cell_dialog_new (EHTMLEditor *editor); +void e_html_editor_cell_dialog_show (EHTMLEditorCellDialog *dialog, + WebKitDOMNode *cell); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_CELL_DIALOG_H */ diff --git a/e-util/e-html-editor-dialog.c b/e-util/e-html-editor-dialog.c new file mode 100644 index 0000000000..5b43ebd316 --- /dev/null +++ b/e-util/e-html-editor-dialog.c @@ -0,0 +1,248 @@ +/* + * e-html-editor-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-dialog.h" + +#define E_HTML_EDITOR_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_DIALOG, EHTMLEditorDialogPrivate)) + +struct _EHTMLEditorDialogPrivate { + EHTMLEditor *editor; + + GtkBox *button_box; + GtkGrid *container; +}; + +enum { + PROP_0, + PROP_EDITOR, +}; + +G_DEFINE_ABSTRACT_TYPE ( + EHTMLEditorDialog, + e_html_editor_dialog, + GTK_TYPE_WINDOW); + +static void +html_editor_dialog_set_editor (EHTMLEditorDialog *dialog, + EHTMLEditor *editor) +{ + dialog->priv->editor = g_object_ref (editor); + + g_object_notify (G_OBJECT (dialog), "editor"); +} + +static void +html_editor_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EDITOR: + g_value_set_object ( + value, + e_html_editor_dialog_get_editor ( + E_HTML_EDITOR_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_EDITOR: + html_editor_dialog_set_editor ( + E_HTML_EDITOR_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_dialog_constructed (GObject *object) +{ + EHTMLEditorDialog *dialog = E_HTML_EDITOR_DIALOG (object); + + /* Chain up to parent implementation first */ + G_OBJECT_CLASS (e_html_editor_dialog_parent_class)->constructed (object); + + gtk_window_set_transient_for ( + GTK_WINDOW (dialog), + GTK_WINDOW (gtk_widget_get_toplevel ( + GTK_WIDGET (dialog->priv->editor)))); +} + +static void +html_editor_dialog_show (GtkWidget *widget) +{ + EHTMLEditorDialogPrivate *priv; + + priv = E_HTML_EDITOR_DIALOG_GET_PRIVATE (widget); + + gtk_widget_show_all (GTK_WIDGET (priv->container)); + gtk_widget_show_all (GTK_WIDGET (priv->button_box)); + + GTK_WIDGET_CLASS (e_html_editor_dialog_parent_class)->show (widget); +} + +static void +html_editor_dialog_dispose (GObject *object) +{ + EHTMLEditorDialogPrivate *priv; + + priv = E_HTML_EDITOR_DIALOG_GET_PRIVATE (object); + + g_clear_object (&priv->editor); + + /* Chain up to parent's implementation */ + G_OBJECT_CLASS (e_html_editor_dialog_parent_class)->dispose (object); +} + +static void +e_html_editor_dialog_class_init (EHTMLEditorDialogClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = html_editor_dialog_get_property; + object_class->set_property = html_editor_dialog_set_property; + object_class->dispose = html_editor_dialog_dispose; + object_class->constructed = html_editor_dialog_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_dialog_show; + + g_object_class_install_property ( + object_class, + PROP_EDITOR, + g_param_spec_object ( + "editor", + NULL, + NULL, + E_TYPE_HTML_EDITOR, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE)); +} + +static gboolean +key_press_event_cb (GtkWidget *widget, + GdkEventKey *event, + gpointer user_data) +{ + if (event->keyval == GDK_KEY_Escape) { + gtk_widget_hide (widget); + return TRUE; + } + + return FALSE; +} + +static void +e_html_editor_dialog_init (EHTMLEditorDialog *dialog) +{ + GtkBox *main_layout; + GtkGrid *grid; + GtkWidget *widget, *button_box; + + dialog->priv = E_HTML_EDITOR_DIALOG_GET_PRIVATE (dialog); + + main_layout = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 5)); + gtk_container_add (GTK_CONTAINER (dialog), GTK_WIDGET (main_layout)); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 10); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 10); + gtk_grid_set_column_spacing (grid, 10); + gtk_box_pack_start (main_layout, GTK_WIDGET (grid), TRUE, TRUE, 5); + dialog->priv->container = grid; + + /* == Button box == */ + widget = gtk_button_new_from_stock (GTK_STOCK_CLOSE); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (gtk_widget_hide), dialog); + + button_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); + gtk_button_box_set_layout (GTK_BUTTON_BOX (button_box), GTK_BUTTONBOX_END); + gtk_box_set_spacing (GTK_BOX (button_box), 5); + gtk_box_pack_start (main_layout, button_box, TRUE, TRUE, 5); + gtk_box_pack_start (GTK_BOX (button_box), widget, FALSE, FALSE, 5); + dialog->priv->button_box = GTK_BOX (button_box); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); + + g_object_set ( + G_OBJECT (dialog), + "destroy-with-parent", TRUE, + "modal", TRUE, + "resizable", FALSE, + "window-position", GTK_WIN_POS_CENTER_ON_PARENT, + NULL); + + /* Don't destroy the dialog when closed! */ + g_signal_connect ( + dialog, "delete-event", + G_CALLBACK (gtk_widget_hide_on_delete), NULL); + + g_signal_connect ( + dialog, "key-press-event", + G_CALLBACK (key_press_event_cb), NULL); +} + +EHTMLEditor * +e_html_editor_dialog_get_editor (EHTMLEditorDialog *dialog) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_DIALOG (dialog), NULL); + + return dialog->priv->editor; +} + +GtkGrid * +e_html_editor_dialog_get_container (EHTMLEditorDialog *dialog) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_DIALOG (dialog), NULL); + + return dialog->priv->container; +} + +GtkBox * +e_html_editor_dialog_get_button_box (EHTMLEditorDialog *dialog) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_DIALOG (dialog), NULL); + + return dialog->priv->button_box; +} + diff --git a/e-util/e-html-editor-dialog.h b/e-util/e-html-editor-dialog.h new file mode 100644 index 0000000000..37fc7a53c3 --- /dev/null +++ b/e-util/e-html-editor-dialog.h @@ -0,0 +1,74 @@ +/* + * e-html-editor-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_DIALOG_H +#define E_HTML_EDITOR_DIALOG_H + +#include <gtk/gtk.h> +#include <e-util/e-html-editor.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_DIALOG \ + (e_html_editor_dialog_get_type ()) +#define E_HTML_EDITOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_DIALOG, EHTMLEditorDialog)) +#define E_HTML_EDITOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_DIALOG, EHTMLEditorDialogClass)) +#define E_IS_HTML_EDITOR_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_DIALOG)) +#define E_IS_HTML_EDITOR_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_DIALOG)) +#define E_HTML_EDITOR_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_DIALOG, EHTMLEditorDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorDialog EHTMLEditorDialog; +typedef struct _EHTMLEditorDialogClass EHTMLEditorDialogClass; +typedef struct _EHTMLEditorDialogPrivate EHTMLEditorDialogPrivate; + +struct _EHTMLEditorDialog { + GtkWindow parent; + EHTMLEditorDialogPrivate *priv; +}; + +struct _EHTMLEditorDialogClass { + GtkWindowClass parent_class; +}; + +GType e_html_editor_dialog_get_type (void) G_GNUC_CONST; +EHTMLEditor * e_html_editor_dialog_get_editor (EHTMLEditorDialog *dialog); +GtkBox * e_html_editor_dialog_get_button_box + (EHTMLEditorDialog *dialog); +GtkGrid * e_html_editor_dialog_get_container + (EHTMLEditorDialog *dialog); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_DIALOG_H */ diff --git a/e-util/e-html-editor-find-dialog.c b/e-util/e-html-editor-find-dialog.c new file mode 100644 index 0000000000..9f44cf8dc5 --- /dev/null +++ b/e-util/e-html-editor-find-dialog.c @@ -0,0 +1,224 @@ +/* + * e-html-editor-find-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-find-dialog.h" + +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> + +#define E_HTML_EDITOR_FIND_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_FIND_DIALOG, EHTMLEditorFindDialogPrivate)) + +struct _EHTMLEditorFindDialogPrivate { + GtkWidget *entry; + GtkWidget *backwards; + GtkWidget *case_sensitive; + GtkWidget *wrap_search; + + GtkWidget *find_button; + + GtkWidget *result_label; +}; + +G_DEFINE_TYPE ( + EHTMLEditorFindDialog, + e_html_editor_find_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +reset_dialog (EHTMLEditorFindDialog *dialog) +{ + gtk_widget_set_sensitive (dialog->priv->find_button, TRUE); + gtk_widget_hide (dialog->priv->result_label); +} + +static void +html_editor_find_dialog_show (GtkWidget *widget) +{ + EHTMLEditorFindDialog *dialog = E_HTML_EDITOR_FIND_DIALOG (widget); + + reset_dialog (dialog); + gtk_widget_grab_focus (dialog->priv->entry); + + /* Chain up to parent's implementation */ + GTK_WIDGET_CLASS (e_html_editor_find_dialog_parent_class)->show (widget); +} + +static void +html_editor_find_dialog_find_cb (EHTMLEditorFindDialog *dialog) +{ + gboolean found; + EHTMLEditor *editor; + EHTMLEditorView *view; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + found = webkit_web_view_search_text ( + WEBKIT_WEB_VIEW (view), + gtk_entry_get_text ( + GTK_ENTRY (dialog->priv->entry)), + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON ( + dialog->priv->case_sensitive)), + !gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON ( + dialog->priv->backwards)), + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON ( + dialog->priv->wrap_search))); + + gtk_widget_set_sensitive (dialog->priv->find_button, found); + + /* We give focus to WebKit so that the selection is highlited. + * Without focus selection is not visible (at least with my default + * color scheme). The focus in fact is not given to WebKit, because + * this dialog is modal, but it satisfies it in a way that it paints + * the selection :) */ + gtk_widget_grab_focus (GTK_WIDGET (view)); + + if (!found) { + gtk_label_set_label ( + GTK_LABEL (dialog->priv->result_label), + N_("No match found")); + gtk_widget_show (dialog->priv->result_label); + } +} + +static gboolean +entry_key_release_event (GtkWidget *widget, + GdkEvent *event, + gpointer user_data) +{ + GdkEventKey *key = &event->key; + EHTMLEditorFindDialog *dialog = user_data; + + if (key->keyval == GDK_KEY_Return) { + html_editor_find_dialog_find_cb (dialog); + return TRUE; + } + + reset_dialog (dialog); + return FALSE; +} + +static void +e_html_editor_find_dialog_class_init (EHTMLEditorFindDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorFindDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_find_dialog_show; +} + +static void +e_html_editor_find_dialog_init (EHTMLEditorFindDialog *dialog) +{ + GtkGrid *main_layout; + GtkBox *box; + GtkWidget *widget; + + dialog->priv = E_HTML_EDITOR_FIND_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + widget = gtk_entry_new (); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + dialog->priv->entry = widget; + g_signal_connect ( + widget, "key-release-event", + G_CALLBACK (entry_key_release_event), dialog); + + box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5)); + gtk_grid_attach (main_layout, GTK_WIDGET (box), 0, 1, 1, 1); + + widget = gtk_check_button_new_with_mnemonic (N_("Search _backwards")); + gtk_box_pack_start (box, widget, FALSE, FALSE, 0); + dialog->priv->backwards = widget; + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (reset_dialog), dialog); + + widget = gtk_check_button_new_with_mnemonic (N_("Case _Sensitive")); + gtk_box_pack_start (box, widget, FALSE, FALSE, 0); + dialog->priv->case_sensitive = widget; + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (reset_dialog), dialog); + + widget = gtk_check_button_new_with_mnemonic (N_("_Wrap Search")); + gtk_box_pack_start (box, widget, FALSE, FALSE, 0); + dialog->priv->wrap_search = widget; + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (reset_dialog), dialog); + + box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5)); + gtk_grid_attach (main_layout, GTK_WIDGET (box), 0, 2, 1, 1); + + widget = gtk_label_new (""); + gtk_box_pack_start (box, widget, FALSE, FALSE, 0); + dialog->priv->result_label = widget; + + widget = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); + gtk_box_set_spacing (GTK_BOX (widget), 5); + gtk_button_box_set_layout (GTK_BUTTON_BOX (widget), GTK_BUTTONBOX_END); + gtk_box_pack_end (box, widget, TRUE, TRUE, 0); + box = GTK_BOX (widget); + + box = e_html_editor_dialog_get_button_box (E_HTML_EDITOR_DIALOG (dialog)); + widget = gtk_button_new_from_stock (GTK_STOCK_FIND); + gtk_box_pack_start (box, widget, FALSE, FALSE, 5); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_find_dialog_find_cb), dialog); + dialog->priv->find_button = widget; + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_find_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_FIND_DIALOG, + "editor", editor, + "icon-name", GTK_STOCK_FIND, + "title", N_("Find"), + NULL)); +} + +void +e_html_editor_find_dialog_find_next (EHTMLEditorFindDialog *dialog) +{ + if (gtk_entry_get_text_length (GTK_ENTRY (dialog->priv->entry)) == 0) { + return; + } + + html_editor_find_dialog_find_cb (dialog); +} diff --git a/e-util/e-html-editor-find-dialog.h b/e-util/e-html-editor-find-dialog.h new file mode 100644 index 0000000000..60134f467f --- /dev/null +++ b/e-util/e-html-editor-find-dialog.h @@ -0,0 +1,73 @@ +/* + * e-html-editor-find-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_FIND_DIALOG_H +#define E_HTML_EDITOR_FIND_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_FIND_DIALOG \ + (e_html_editor_find_dialog_get_type ()) +#define E_HTML_EDITOR_FIND_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_FIND_DIALOG, EHTMLEditorFindDialog)) +#define E_HTML_EDITOR_FIND_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_FIND_DIALOG, EHTMLEditorFindDialogClass)) +#define E_IS_HTML_EDITOR_FIND_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_FIND_DIALOG)) +#define E_IS_HTML_EDITOR_FIND_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_FIND_DIALOG)) +#define E_HTML_EDITOR_FIND_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_FIND_DIALOG, EHTMLEditorFindDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorFindDialog EHTMLEditorFindDialog; +typedef struct _EHTMLEditorFindDialogClass EHTMLEditorFindDialogClass; +typedef struct _EHTMLEditorFindDialogPrivate EHTMLEditorFindDialogPrivate; + +struct _EHTMLEditorFindDialog { + EHTMLEditorDialog parent; + EHTMLEditorFindDialogPrivate *priv; +}; + +struct _EHTMLEditorFindDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_find_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_find_dialog_new (EHTMLEditor *editor); +void e_html_editor_find_dialog_find_next + (EHTMLEditorFindDialog *dialog); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_FIND_DIALOG_H */ + diff --git a/e-util/e-html-editor-hrule-dialog.c b/e-util/e-html-editor-hrule-dialog.c new file mode 100644 index 0000000000..9ace655812 --- /dev/null +++ b/e-util/e-html-editor-hrule-dialog.c @@ -0,0 +1,421 @@ +/* + * e-html-editor-hrule-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-hrule-dialog.h" +#include "e-html-editor-utils.h" +#include "e-html-editor-view.h" + +#include <glib/gi18n-lib.h> +#include <webkit/webkitdom.h> +#include <stdlib.h> + +#define E_HTML_EDITOR_HRULE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_HRULE_DIALOG, EHTMLEditorHRuleDialogPrivate)) + +struct _EHTMLEditorHRuleDialogPrivate { + GtkWidget *width_edit; + GtkWidget *size_edit; + GtkWidget *unit_combo; + + GtkWidget *alignment_combo; + GtkWidget *shaded_check; + + WebKitDOMHTMLHRElement *hr_element; +}; + +G_DEFINE_TYPE ( + EHTMLEditorHRuleDialog, + e_html_editor_hrule_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +html_editor_hrule_dialog_set_alignment (EHTMLEditorHRuleDialog *dialog) +{ + const gchar *alignment; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + alignment = gtk_combo_box_get_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo)); + + webkit_dom_htmlhr_element_set_align (dialog->priv->hr_element, alignment); +} + +static void +html_editor_hrule_dialog_get_alignment (EHTMLEditorHRuleDialog *dialog) +{ + gchar *alignment; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + alignment = webkit_dom_htmlhr_element_get_align (dialog->priv->hr_element); + + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo), alignment); + g_free (alignment); +} + +static void +html_editor_hrule_dialog_set_size (EHTMLEditorHRuleDialog *dialog) +{ + gchar *size; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + size = g_strdup_printf ( + "%d", + (gint) gtk_spin_button_get_value ( + GTK_SPIN_BUTTON (dialog->priv->size_edit))); + + webkit_dom_htmlhr_element_set_size (dialog->priv->hr_element, size); + + g_free (size); +} + +static void +html_editor_hrule_dialog_get_size (EHTMLEditorHRuleDialog *dialog) +{ + gchar *size; + gint size_int = 0; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + size = webkit_dom_htmlhr_element_get_size (dialog->priv->hr_element); + if (size && *size) { + size_int = atoi (size); + } + + if (size_int == 0) { + size_int = 2; + } + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->size_edit), (gdouble) size_int); + + g_free (size); +} + +static void +html_editor_hrule_dialog_set_width (EHTMLEditorHRuleDialog *dialog) +{ + gchar *width, *units; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + units = gtk_combo_box_text_get_active_text ( + GTK_COMBO_BOX_TEXT (dialog->priv->unit_combo)); + width = g_strdup_printf ( + "%d%s", + (gint) gtk_spin_button_get_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit)), + units); + + webkit_dom_htmlhr_element_set_width (dialog->priv->hr_element, width); + + g_free (units); + g_free (width); +} + +static void +html_editor_hrule_dialog_get_width (EHTMLEditorHRuleDialog *dialog) +{ + gchar *width; + const gchar *units; + gint width_int = 0; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + width = webkit_dom_htmlhr_element_get_width (dialog->priv->hr_element); + if (width && *width) { + width_int = atoi (width); + + if (strstr (width, "%") != NULL) { + units = "units-percent"; + } else { + units = "units-px"; + } + } + + if (width_int == 0) { + width_int = 100; + units = "units-percent"; + } + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), (gdouble) width_int); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->unit_combo), units); + + g_free (width); +} + +static void +html_editor_hrule_dialog_set_shading (EHTMLEditorHRuleDialog *dialog) +{ + gboolean no_shade; + + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + no_shade = !gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->shaded_check)); + + webkit_dom_htmlhr_element_set_no_shade (dialog->priv->hr_element, no_shade); +} + +static void +html_editor_hrule_dialog_get_shading (EHTMLEditorHRuleDialog *dialog) +{ + g_return_if_fail (WEBKIT_DOM_IS_HTMLHR_ELEMENT (dialog->priv->hr_element)); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->shaded_check), + !webkit_dom_htmlhr_element_get_no_shade (dialog->priv->hr_element)); +} + +static void +html_editor_hrule_dialog_hide (GtkWidget *widget) +{ + EHTMLEditorHRuleDialogPrivate *priv; + + priv = E_HTML_EDITOR_HRULE_DIALOG_GET_PRIVATE (widget); + + priv->hr_element = NULL; + + GTK_WIDGET_CLASS (e_html_editor_hrule_dialog_parent_class)->hide (widget); +} + +static void +html_editor_hrule_dialog_show (GtkWidget *widget) +{ + EHTMLEditorHRuleDialog *dialog; + EHTMLEditor *editor; + EHTMLEditorSelection *editor_selection; + EHTMLEditorView *view; + + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *selection; + WebKitDOMElement *rule; + + dialog = E_HTML_EDITOR_HRULE_DIALOG (widget); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + editor_selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + selection = webkit_dom_dom_window_get_selection (window); + if (webkit_dom_dom_selection_get_range_count (selection) < 1) { + GTK_WIDGET_CLASS (e_html_editor_hrule_dialog_parent_class)->show (widget); + return; + } + + rule = e_html_editor_view_get_element_under_mouse_click (view); + if (!rule) { + WebKitDOMElement *caret, *parent, *element; + + caret = e_html_editor_selection_save_caret_position (editor_selection); + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (caret)); + element = caret; + + while (!WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + element = parent; + parent = webkit_dom_node_get_parent_element ( + WEBKIT_DOM_NODE (parent)); + } + + rule = webkit_dom_document_create_element (document, "HR", NULL); + + /* Insert horizontal rule into body below the caret */ + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (rule), + webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (element)), + NULL); + + e_html_editor_selection_clear_caret_position_marker (editor_selection); + + dialog->priv->hr_element = WEBKIT_DOM_HTMLHR_ELEMENT (rule); + + /* For new rule reset the values to default */ + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), 100.0); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->unit_combo), "units-percent"); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->size_edit), 2.0); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo), "left"); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->shaded_check), FALSE); + + html_editor_hrule_dialog_set_alignment (dialog); + html_editor_hrule_dialog_set_size (dialog); + html_editor_hrule_dialog_set_alignment (dialog); + html_editor_hrule_dialog_set_shading (dialog); + + e_html_editor_view_set_changed (view, TRUE); + } else { + dialog->priv->hr_element = WEBKIT_DOM_HTMLHR_ELEMENT (rule); + + html_editor_hrule_dialog_get_alignment (dialog); + html_editor_hrule_dialog_get_size (dialog); + html_editor_hrule_dialog_get_width (dialog); + html_editor_hrule_dialog_get_shading (dialog); + } + + /* Chain up to parent implementation */ + GTK_WIDGET_CLASS (e_html_editor_hrule_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_hrule_dialog_class_init (EHTMLEditorHRuleDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorHRuleDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_hrule_dialog_show; + widget_class->hide = html_editor_hrule_dialog_hide; +} + +static void +e_html_editor_hrule_dialog_init (EHTMLEditorHRuleDialog *dialog) +{ + GtkGrid *main_layout, *grid; + GtkWidget *widget; + + dialog->priv = E_HTML_EDITOR_HRULE_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == Size == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Size</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_set_row_spacing (grid, 5); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + + /* Width */ + widget = gtk_spin_button_new_with_range (0.0, 100.0, 1.0); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (widget), 100); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_hrule_dialog_set_width), dialog); + dialog->priv->width_edit = widget; + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + + widget = gtk_label_new_with_mnemonic (_("_Width:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->size_edit); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-px", "px"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-percent", "%"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (widget), "units-percent"); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_hrule_dialog_set_width), dialog); + dialog->priv->unit_combo = widget; + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + + /* Size */ + widget = gtk_spin_button_new_with_range (0.0, 100.0, 1.0); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (widget), 2); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_hrule_dialog_set_size), dialog); + dialog->priv->size_edit = widget; + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + + widget = gtk_label_new_with_mnemonic (_("_Size:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->size_edit); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + /* == Style == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Style</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_set_row_spacing (grid, 5); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + + /* Alignment */ + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append ( + GTK_COMBO_BOX_TEXT (widget), "left", _("Left")); + gtk_combo_box_text_append ( + GTK_COMBO_BOX_TEXT (widget), "center", _("Center")); + gtk_combo_box_text_append ( + GTK_COMBO_BOX_TEXT (widget), "right", _("Right")); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (widget), "left"); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_hrule_dialog_set_alignment), dialog); + dialog->priv->alignment_combo = widget; + gtk_grid_attach (grid, widget, 1, 0, 2, 1); + + widget = gtk_label_new_with_mnemonic (_("_Alignment:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), widget); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Shaded */ + widget = gtk_check_button_new_with_mnemonic (_("S_haded")); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_hrule_dialog_set_shading), dialog); + dialog->priv->shaded_check = widget; + gtk_grid_attach (grid, widget, 0, 1, 2, 1); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_hrule_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_HRULE_DIALOG, + "editor", editor, + "title", _("Rule properties"), + NULL)); +} diff --git a/e-util/e-html-editor-hrule-dialog.h b/e-util/e-html-editor-hrule-dialog.h new file mode 100644 index 0000000000..876fc2515c --- /dev/null +++ b/e-util/e-html-editor-hrule-dialog.h @@ -0,0 +1,70 @@ +/* + * e-html-editor-hrule-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_HRULE_DIALOG_H +#define E_HTML_EDITOR_HRULE_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_HRULE_DIALOG \ + (e_html_editor_hrule_dialog_get_type ()) +#define E_HTML_EDITOR_HRULE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_HRULE_DIALOG, EHTMLEditorHRuleDialog)) +#define E_HTML_EDITOR_HRULE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_HRULE_DIALOG, EHTMLEditorHRuleDialogClass)) +#define E_IS_HTML_EDITOR_HRULE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_HRULE_DIALOG)) +#define E_IS_HTML_EDITOR_HRULE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_HRULE_DIALOG)) +#define E_HTML_EDITOR_HRULE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_HRULE_DIALOG, EHTMLEditorHRuleDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorHRuleDialog EHTMLEditorHRuleDialog; +typedef struct _EHTMLEditorHRuleDialogClass EHTMLEditorHRuleDialogClass; +typedef struct _EHTMLEditorHRuleDialogPrivate EHTMLEditorHRuleDialogPrivate; + +struct _EHTMLEditorHRuleDialog { + EHTMLEditorDialog parent; + EHTMLEditorHRuleDialogPrivate *priv; +}; + +struct _EHTMLEditorHRuleDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_hrule_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_hrule_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_HRULE_DIALOG_H */ diff --git a/e-util/e-html-editor-image-dialog.c b/e-util/e-html-editor-image-dialog.c new file mode 100644 index 0000000000..346de4419b --- /dev/null +++ b/e-util/e-html-editor-image-dialog.c @@ -0,0 +1,703 @@ +/* + * e-html-editor-image-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-image-dialog.h" + +#include <stdlib.h> +#include <glib/gi18n-lib.h> + +#include "e-html-editor-utils.h" +#include "e-image-chooser-dialog.h" + +#define E_HTML_EDITOR_IMAGE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_IMAGE_DIALOG, EHTMLEditorImageDialogPrivate)) + +struct _EHTMLEditorImageDialogPrivate { + GtkWidget *file_chooser; + GtkWidget *description_edit; + + GtkWidget *width_edit; + GtkWidget *width_units; + GtkWidget *height_edit; + GtkWidget *height_units; + GtkWidget *alignment; + + GtkWidget *x_padding_edit; + GtkWidget *y_padding_edit; + GtkWidget *border_edit; + + GtkWidget *url_edit; + GtkWidget *test_url_button; + + WebKitDOMHTMLImageElement *image; +}; + +G_DEFINE_TYPE ( + EHTMLEditorImageDialog, + e_html_editor_image_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +html_editor_image_dialog_set_src (EHTMLEditorImageDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorSelection *editor_selection; + EHTMLEditorView *view; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + editor_selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_replace_image_src ( + editor_selection, + WEBKIT_DOM_ELEMENT (dialog->priv->image), + gtk_file_chooser_get_uri ( + GTK_FILE_CHOOSER (dialog->priv->file_chooser))); +} + +static void +html_editor_image_dialog_set_alt (EHTMLEditorImageDialog *dialog) +{ + webkit_dom_html_image_element_set_alt ( + dialog->priv->image, + gtk_entry_get_text (GTK_ENTRY (dialog->priv->description_edit))); +} + +static void +html_editor_image_dialog_set_width (EHTMLEditorImageDialog *dialog) +{ + gint requested; + gulong natural; + gint width; + + natural = webkit_dom_html_image_element_get_natural_width ( + dialog->priv->image); + requested = gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->width_edit)); + + switch (gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->width_units))) { + + case 0: /* px */ + width = requested; + break; + + case 1: /* percent */ + width = natural * requested * 0.01; + break; + + case 2: /* follow */ + width = natural; + break; + + } + + webkit_dom_html_image_element_set_width (dialog->priv->image, width); +} + +static void +html_editor_image_dialog_set_width_units (EHTMLEditorImageDialog *dialog) +{ + gint requested; + gulong natural; + gint width = 0; + + natural = webkit_dom_html_image_element_get_natural_width ( + dialog->priv->image); + requested = gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->width_edit)); + + switch (gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->width_units))) { + + case 0: /* px */ + if (gtk_widget_is_sensitive (dialog->priv->width_edit)) { + width = requested * natural * 0.01; + } else { + width = natural; + } + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), "style"); + gtk_widget_set_sensitive (dialog->priv->width_edit, TRUE); + break; + + case 1: /* percent */ + if (gtk_widget_is_sensitive (dialog->priv->width_edit)) { + width = (((gdouble) requested) / natural) * 100; + } else { + width = 100; + } + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), "style"); + gtk_widget_set_sensitive (dialog->priv->width_edit, TRUE); + break; + + case 2: /* follow */ + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), + "style", + "width: auto;", + NULL); + gtk_widget_set_sensitive (dialog->priv->width_edit, FALSE); + break; + } + + if (width != 0) { + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), width); + } +} + +static void +html_editor_image_dialog_set_height (EHTMLEditorImageDialog *dialog) +{ + gint requested; + gulong natural; + gint height; + + natural = webkit_dom_html_image_element_get_natural_height ( + dialog->priv->image); + requested = gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->height_edit)); + + switch (gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->height_units))) { + + case 0: /* px */ + height = requested; + break; + + case 1: /* percent */ + height = natural * requested * 0.01; + break; + + case 2: /* follow */ + height = natural; + break; + + } + + webkit_dom_html_image_element_set_height (dialog->priv->image, height); +} + +static void +html_editor_image_dialog_set_height_units (EHTMLEditorImageDialog *dialog) +{ + gint requested; + gulong natural; + gint height = -1; + + natural = webkit_dom_html_image_element_get_natural_height ( + dialog->priv->image); + requested = gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->height_edit)); + + switch (gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->height_units))) { + + case 0: /* px */ + if (gtk_widget_is_sensitive (dialog->priv->height_edit)) { + height = requested * natural * 0.01; + } else { + height = natural; + } + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), "style"); + gtk_widget_set_sensitive (dialog->priv->height_edit, TRUE); + break; + + case 1: /* percent */ + if (gtk_widget_is_sensitive (dialog->priv->height_edit)) { + height = (((gdouble) requested) / natural) * 100; + } else { + height = 100; + } + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), "style"); + gtk_widget_set_sensitive (dialog->priv->height_edit, TRUE); + break; + + case 2: /* follow */ + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), + "style", + "height: auto;", + NULL); + gtk_widget_set_sensitive (dialog->priv->height_edit, FALSE); + break; + } + + if (height != -1) { + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->height_edit), height); + } +} + +static void +html_editor_image_dialog_set_alignment (EHTMLEditorImageDialog *dialog) +{ + webkit_dom_html_image_element_set_align ( + dialog->priv->image, + gtk_combo_box_get_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment))); +} + +static void +html_editor_image_dialog_set_x_padding (EHTMLEditorImageDialog *dialog) +{ + webkit_dom_html_image_element_set_hspace ( + dialog->priv->image, + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->x_padding_edit))); +} + +static void +html_editor_image_dialog_set_y_padding (EHTMLEditorImageDialog *dialog) +{ + webkit_dom_html_image_element_set_vspace ( + dialog->priv->image, + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->y_padding_edit))); +} + +static void +html_editor_image_dialog_set_border (EHTMLEditorImageDialog *dialog) +{ + gchar *val; + + val = g_strdup_printf ( + "%d", gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->border_edit))); + + webkit_dom_html_image_element_set_border (dialog->priv->image, val); + + g_free (val); +} + +static void +html_editor_image_dialog_set_url (EHTMLEditorImageDialog *dialog) +{ + WebKitDOMElement *link; + const gchar *url; + + url = gtk_entry_get_text (GTK_ENTRY (dialog->priv->url_edit)); + link = e_html_editor_dom_node_find_parent_element ( + WEBKIT_DOM_NODE (dialog->priv->image), "A"); + + if (link) { + if (!url || !*url) { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (link)), + WEBKIT_DOM_NODE (dialog->priv->image), + WEBKIT_DOM_NODE (link), NULL); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (link)), + WEBKIT_DOM_NODE (link), NULL); + } else { + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (link), url); + } + } else { + if (url && *url) { + WebKitDOMDocument *document; + + document = webkit_dom_node_get_owner_document ( + WEBKIT_DOM_NODE (dialog->priv->image)); + link = webkit_dom_document_create_element ( + document, "A", NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (link), url); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (dialog->priv->image)), + WEBKIT_DOM_NODE (link), + WEBKIT_DOM_NODE (dialog->priv->image), NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (link), + WEBKIT_DOM_NODE (dialog->priv->image), NULL); + } + } +} + +static void +html_editor_image_dialog_test_url (EHTMLEditorImageDialog *dialog) +{ + gtk_show_uri ( + gtk_window_get_screen (GTK_WINDOW (dialog)), + gtk_entry_get_text (GTK_ENTRY (dialog->priv->url_edit)), + GDK_CURRENT_TIME, + NULL); +} + +static void +html_editor_image_dialog_show (GtkWidget *widget) +{ + EHTMLEditorImageDialog *dialog; + WebKitDOMElement *link; + gchar *tmp; + glong val; + + dialog = E_HTML_EDITOR_IMAGE_DIALOG (widget); + + if (!dialog->priv->image) { + return; + } + + tmp = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->image), "data-uri"); + if (tmp && *tmp) { + gtk_file_chooser_set_uri ( + GTK_FILE_CHOOSER (dialog->priv->file_chooser), tmp); + gtk_widget_set_sensitive ( + GTK_WIDGET (dialog->priv->file_chooser), TRUE); + g_free (tmp); + } else { + gtk_file_chooser_set_uri ( + GTK_FILE_CHOOSER (dialog->priv->file_chooser), ""); + gtk_widget_set_sensitive ( + GTK_WIDGET (dialog->priv->file_chooser), FALSE); + } + + tmp = webkit_dom_html_image_element_get_alt (dialog->priv->image); + gtk_entry_set_text (GTK_ENTRY (dialog->priv->description_edit), tmp ? tmp : ""); + g_free (tmp); + + val = webkit_dom_html_image_element_get_width (dialog->priv->image); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), val); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->width_units), "units-px"); + + val = webkit_dom_html_image_element_get_height (dialog->priv->image); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->height_edit), val); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->height_units), "units-px"); + + tmp = webkit_dom_html_image_element_get_border (dialog->priv->image); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment), + (tmp && *tmp) ? tmp : "bottom"); + g_free (tmp); + + val = webkit_dom_html_image_element_get_hspace (dialog->priv->image); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->x_padding_edit), val); + + val = webkit_dom_html_image_element_get_vspace (dialog->priv->image); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->y_padding_edit), val); + + link = e_html_editor_dom_node_find_parent_element ( + WEBKIT_DOM_NODE (dialog->priv->image), "A"); + if (link) { + tmp = webkit_dom_html_anchor_element_get_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (link)); + gtk_entry_set_text (GTK_ENTRY (dialog->priv->url_edit), tmp); + g_free (tmp); + } + + /* Chain up to parent implementation */ + GTK_WIDGET_CLASS (e_html_editor_image_dialog_parent_class)->show (widget); +} + +static void +html_editor_image_dialog_hide (GtkWidget *widget) +{ + EHTMLEditorImageDialogPrivate *priv; + + priv = E_HTML_EDITOR_IMAGE_DIALOG_GET_PRIVATE (widget); + + priv->image = NULL; + + GTK_WIDGET_CLASS (e_html_editor_image_dialog_parent_class)->hide (widget); +} + +static void +e_html_editor_image_dialog_class_init (EHTMLEditorImageDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorImageDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_image_dialog_show; + widget_class->hide = html_editor_image_dialog_hide; +} + +static void +e_html_editor_image_dialog_init (EHTMLEditorImageDialog *dialog) +{ + GtkGrid *main_layout, *grid; + GtkWidget *widget; + GtkFileFilter *file_filter; + + dialog->priv = E_HTML_EDITOR_IMAGE_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == General == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>General</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Source */ + widget = e_image_chooser_dialog_new ( + _("Choose Background Image"), + GTK_WINDOW (dialog)); + gtk_file_chooser_set_action ( + GTK_FILE_CHOOSER (widget), GTK_FILE_CHOOSER_ACTION_OPEN); + + file_filter = gtk_file_filter_new (); + gtk_file_filter_set_name (file_filter, _("Images")); + gtk_file_filter_add_mime_type (file_filter, "image/*"); + + widget = gtk_file_chooser_button_new_with_dialog (widget); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "file-set", + G_CALLBACK (html_editor_image_dialog_set_src), dialog); + dialog->priv->file_chooser = widget; + + widget = gtk_label_new_with_mnemonic (_("_Source:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->file_chooser); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Description */ + widget = gtk_entry_new (); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + g_signal_connect_swapped ( + widget, "notify::text", + G_CALLBACK (html_editor_image_dialog_set_alt), dialog); + dialog->priv->description_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Description:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->description_edit); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + /* == Layout == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Layout</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Width */ + widget = gtk_spin_button_new_with_range (1, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_image_dialog_set_width), dialog); + dialog->priv->width_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Width:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->width_edit); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-px", "px"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-percent", "%"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-follow", "follow"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (widget), "units-px"); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_image_dialog_set_width_units), dialog); + dialog->priv->width_units = widget; + + /* Height */ + widget = gtk_spin_button_new_with_range (1, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_image_dialog_set_height), dialog); + dialog->priv->height_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Height:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->height_edit); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-px", "px"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-percent", "%"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-follow", "follow"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (widget), "units-px"); + gtk_grid_attach (grid, widget, 2, 1, 1, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_image_dialog_set_height_units), dialog); + dialog->priv->height_units = widget; + + /* Alignment */ + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "top", _("Top")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "middle", _("Middle")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "bottom", _("Bottom")); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (widget), "bottom"); + gtk_grid_attach (grid, widget, 1, 2, 1, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_image_dialog_set_alignment), dialog); + dialog->priv->alignment = widget; + + widget = gtk_label_new_with_mnemonic (_("_Alignment")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->alignment); + gtk_grid_attach (grid, widget, 0, 2, 1, 1); + + /* X Padding */ + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 5, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_image_dialog_set_x_padding), dialog); + dialog->priv->x_padding_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_X-Padding:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->x_padding_edit); + gtk_grid_attach (grid, widget, 4, 0, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 0, 1, 1); + + /* Y Padding */ + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 5, 1, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_image_dialog_set_y_padding), dialog); + dialog->priv->y_padding_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Y-Padding:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->y_padding_edit); + gtk_grid_attach (grid, widget, 4, 1, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 1, 1, 1); + + /* Border */ + widget = gtk_spin_button_new_with_range (0, G_MAXUINT, 1); + gtk_grid_attach (grid, widget, 5, 2, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_image_dialog_set_border), dialog); + dialog->priv->border_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Border:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->border_edit); + gtk_grid_attach (grid, widget, 4, 2, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 2, 1, 1); + + /* == Layout == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Link</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 4, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 5, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + widget = gtk_entry_new (); + gtk_grid_attach (grid, widget, 1 ,0, 1, 1); + gtk_widget_set_hexpand (widget, TRUE); + g_signal_connect_swapped ( + widget, "notify::text", + G_CALLBACK (html_editor_image_dialog_set_url), dialog); + dialog->priv->url_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_URL:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->url_edit); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + widget = gtk_button_new_with_mnemonic (_("_Test URL...")); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_image_dialog_test_url), dialog); + dialog->priv->test_url_button = widget; + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_image_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_IMAGE_DIALOG, + "editor", editor, + "title", N_("Image Properties"), + NULL)); +} + +void +e_html_editor_image_dialog_show (EHTMLEditorImageDialog *dialog, + WebKitDOMNode *image) +{ + EHTMLEditorImageDialogClass *class; + + g_return_if_fail (E_IS_HTML_EDITOR_IMAGE_DIALOG (dialog)); + + if (image) { + dialog->priv->image = WEBKIT_DOM_HTML_IMAGE_ELEMENT (image); + } else { + dialog->priv->image = NULL; + } + + class = E_HTML_EDITOR_IMAGE_DIALOG_GET_CLASS (dialog); + GTK_WIDGET_CLASS (class)->show (GTK_WIDGET (dialog)); +} diff --git a/e-util/e-html-editor-image-dialog.h b/e-util/e-html-editor-image-dialog.h new file mode 100644 index 0000000000..efdbaf963c --- /dev/null +++ b/e-util/e-html-editor-image-dialog.h @@ -0,0 +1,73 @@ +/* + * e-html-editor-image-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_IMAGE_DIALOG_H +#define E_HTML_EDITOR_IMAGE_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_IMAGE_DIALOG \ + (e_html_editor_image_dialog_get_type ()) +#define E_HTML_EDITOR_IMAGE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_IMAGE_DIALOG, EHTMLEditorImageDialog)) +#define E_HTML_EDITOR_IMAGE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_IMAGE_DIALOG, EHTMLEditorImageDialogClass)) +#define E_IS_HTML_EDITOR_IMAGE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_IMAGE_DIALOG)) +#define E_IS_HTML_EDITOR_IMAGE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_IMAGE_DIALOG)) +#define E_HTML_EDITOR_IMAGE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_IMAGE_DIALOG, EHTMLEditorImageDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorImageDialog EHTMLEditorImageDialog; +typedef struct _EHTMLEditorImageDialogClass EHTMLEditorImageDialogClass; +typedef struct _EHTMLEditorImageDialogPrivate EHTMLEditorImageDialogPrivate; + +struct _EHTMLEditorImageDialog { + EHTMLEditorDialog parent; + EHTMLEditorImageDialogPrivate *priv; +}; + +struct _EHTMLEditorImageDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_image_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_image_dialog_new (EHTMLEditor *editor); +void e_html_editor_image_dialog_show (EHTMLEditorImageDialog *dialog, + WebKitDOMNode *image); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_IMAGE_DIALOG_H */ + diff --git a/e-util/e-html-editor-link-dialog.c b/e-util/e-html-editor-link-dialog.c new file mode 100644 index 0000000000..0572d07c09 --- /dev/null +++ b/e-util/e-html-editor-link-dialog.c @@ -0,0 +1,390 @@ +/* + * e-html-editor-link-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-link-dialog.h" +#include "e-html-editor-selection.h" +#include "e-html-editor-utils.h" +#include "e-html-editor-view.h" + +#include <glib/gi18n-lib.h> + +#define E_HTML_EDITOR_LINK_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_LINK_DIALOG, EHTMLEditorLinkDialogPrivate)) + +G_DEFINE_TYPE ( + EHTMLEditorLinkDialog, + e_html_editor_link_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +struct _EHTMLEditorLinkDialogPrivate { + GtkWidget *url_edit; + GtkWidget *label_edit; + GtkWidget *test_button; + + GtkWidget *remove_link_button; + GtkWidget *ok_button; + + gboolean label_autofill; +}; + +static void +html_editor_link_dialog_test_link (EHTMLEditorLinkDialog *dialog) +{ + gtk_show_uri ( + gtk_window_get_screen (GTK_WINDOW (dialog)), + gtk_entry_get_text (GTK_ENTRY (dialog->priv->url_edit)), + GDK_CURRENT_TIME, + NULL); +} + +static void +html_editor_link_dialog_url_changed (EHTMLEditorLinkDialog *dialog) +{ + if (dialog->priv->label_autofill && + gtk_widget_is_sensitive (dialog->priv->label_edit)) { + const gchar *text; + + text = gtk_entry_get_text ( + GTK_ENTRY (dialog->priv->url_edit)); + gtk_entry_set_text ( + GTK_ENTRY (dialog->priv->label_edit), text); + } +} + +static gboolean +html_editor_link_dialog_description_changed (EHTMLEditorLinkDialog *dialog) +{ + const gchar *text; + + text = gtk_entry_get_text (GTK_ENTRY (dialog->priv->label_edit)); + dialog->priv->label_autofill = (*text == '\0'); + + return FALSE; +} + +static void +html_editor_link_dialog_remove_link (EHTMLEditorLinkDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + e_html_editor_selection_unlink (selection); + + gtk_widget_hide (GTK_WIDGET (dialog)); +} + +static void +html_editor_link_dialog_ok (EHTMLEditorLinkDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range; + WebKitDOMElement *link; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + if (!dom_selection || + (webkit_dom_dom_selection_get_range_count (dom_selection) == 0)) { + gtk_widget_hide (GTK_WIDGET (dialog)); + return; + } + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + link = e_html_editor_dom_node_find_parent_element ( + webkit_dom_range_get_start_container (range, NULL), "A"); + if (!link) { + if ((webkit_dom_range_get_start_container (range, NULL) != + webkit_dom_range_get_end_container (range, NULL)) || + (webkit_dom_range_get_start_offset (range, NULL) != + webkit_dom_range_get_end_offset (range, NULL))) { + + WebKitDOMDocumentFragment *fragment; + fragment = webkit_dom_range_extract_contents (range, NULL); + link = e_html_editor_dom_node_find_child_element ( + WEBKIT_DOM_NODE (fragment), "A"); + webkit_dom_range_insert_node ( + range, WEBKIT_DOM_NODE (fragment), NULL); + + webkit_dom_dom_selection_set_base_and_extent ( + dom_selection, + webkit_dom_range_get_start_container (range, NULL), + webkit_dom_range_get_start_offset (range, NULL), + webkit_dom_range_get_end_container (range, NULL), + webkit_dom_range_get_end_offset (range, NULL), + NULL); + } else { + /* get element that was clicked on */ + link = e_html_editor_view_get_element_under_mouse_click (view); + if (!WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (link)) + link = NULL; + } + } + + if (link) { + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (link), + gtk_entry_get_text (GTK_ENTRY (dialog->priv->url_edit))); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (link), + gtk_entry_get_text (GTK_ENTRY (dialog->priv->label_edit)), + NULL); + } else { + gchar *text; + + /* Check whether a text is selected or not */ + text = webkit_dom_range_get_text (range); + if (text && *text) { + e_html_editor_selection_create_link ( + selection, + gtk_entry_get_text ( + GTK_ENTRY (dialog->priv->url_edit))); + } else { + gchar *html = g_strdup_printf ( + "<a href=\"%s\">%s</a>", + gtk_entry_get_text ( + GTK_ENTRY (dialog->priv->url_edit)), + gtk_entry_get_text ( + GTK_ENTRY (dialog->priv->label_edit))); + + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, html); + + g_free (html); + + } + + g_free (text); + } + + gtk_widget_hide (GTK_WIDGET (dialog)); +} + +static gboolean +html_editor_link_dialog_entry_key_pressed (EHTMLEditorLinkDialog *dialog, + GdkEventKey *event) +{ + /* We can't do thins in key_released, because then you could not open + * this dialog from main menu by pressing enter on Insert->Link action */ + if (event->keyval == GDK_KEY_Return) { + html_editor_link_dialog_ok (dialog); + return TRUE; + } + + return FALSE; +} + +static void +html_editor_link_dialog_show (GtkWidget *widget) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorLinkDialog *dialog; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range; + WebKitDOMElement *link; + + dialog = E_HTML_EDITOR_LINK_DIALOG (widget); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + /* Reset to default values */ + gtk_entry_set_text (GTK_ENTRY (dialog->priv->url_edit), "http://"); + gtk_entry_set_text (GTK_ENTRY (dialog->priv->label_edit), ""); + gtk_widget_set_sensitive (dialog->priv->label_edit, TRUE); + gtk_widget_set_sensitive (dialog->priv->remove_link_button, TRUE); + dialog->priv->label_autofill = TRUE; + + /* No selection at all */ + if (!dom_selection || + webkit_dom_dom_selection_get_range_count (dom_selection) < 1) { + gtk_widget_set_sensitive (dialog->priv->remove_link_button, FALSE); + goto chainup; + } + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + link = e_html_editor_dom_node_find_parent_element ( + webkit_dom_range_get_start_container (range, NULL), "A"); + if (!link) { + if ((webkit_dom_range_get_start_container (range, NULL) != + webkit_dom_range_get_end_container (range, NULL)) || + (webkit_dom_range_get_start_offset (range, NULL) != + webkit_dom_range_get_end_offset (range, NULL))) { + + WebKitDOMDocumentFragment *fragment; + fragment = webkit_dom_range_clone_contents (range, NULL); + link = e_html_editor_dom_node_find_child_element ( + WEBKIT_DOM_NODE (fragment), "A"); + } else { + /* get element that was clicked on */ + link = e_html_editor_view_get_element_under_mouse_click (view); + if (!WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (link)) + link = NULL; + } + } + + if (link) { + gchar *href, *text; + + href = webkit_dom_html_anchor_element_get_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (link)); + text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (link)); + + gtk_entry_set_text ( + GTK_ENTRY (dialog->priv->url_edit), href); + gtk_entry_set_text ( + GTK_ENTRY (dialog->priv->label_edit), text); + + g_free (text); + g_free (href); + } else { + gchar *text; + + text = webkit_dom_range_get_text (range); + if (text && *text) { + gtk_entry_set_text ( + GTK_ENTRY (dialog->priv->label_edit), text); + gtk_widget_set_sensitive ( + dialog->priv->label_edit, FALSE); + gtk_widget_set_sensitive ( + dialog->priv->remove_link_button, FALSE); + } + g_free (text); + } + + chainup: + /* Chain up to parent implementation */ + GTK_WIDGET_CLASS (e_html_editor_link_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_link_dialog_class_init (EHTMLEditorLinkDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorLinkDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_link_dialog_show; +} + +static void +e_html_editor_link_dialog_init (EHTMLEditorLinkDialog *dialog) +{ + GtkGrid *main_layout; + GtkBox *button_box; + GtkWidget *widget; + + dialog->priv = E_HTML_EDITOR_LINK_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + widget = gtk_entry_new (); + gtk_grid_attach (main_layout, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "notify::text", + G_CALLBACK (html_editor_link_dialog_url_changed), dialog); + g_signal_connect_swapped ( + widget, "key-press-event", + G_CALLBACK (html_editor_link_dialog_entry_key_pressed), dialog); + dialog->priv->url_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_URL:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->url_edit); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + widget = gtk_button_new_with_mnemonic (_("_Test URL...")); + gtk_grid_attach (main_layout, widget, 2, 0, 1, 1); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_link_dialog_test_link), dialog); + dialog->priv->test_button = widget; + + widget = gtk_entry_new (); + gtk_grid_attach (main_layout, widget, 1, 1, 2, 1); + g_signal_connect_swapped ( + widget, "key-release-event", + G_CALLBACK (html_editor_link_dialog_description_changed), dialog); + g_signal_connect_swapped ( + widget, "key-press-event", + G_CALLBACK (html_editor_link_dialog_entry_key_pressed), dialog); + dialog->priv->label_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Description:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->label_edit); + gtk_grid_attach (main_layout, widget, 0, 1, 1, 1); + + button_box = e_html_editor_dialog_get_button_box (E_HTML_EDITOR_DIALOG (dialog)); + + widget = gtk_button_new_with_mnemonic (_("_Remove Link")); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_link_dialog_remove_link), dialog); + gtk_box_pack_start (button_box, widget, FALSE, FALSE, 5); + dialog->priv->remove_link_button = widget; + + widget = gtk_button_new_from_stock (GTK_STOCK_OK); + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_link_dialog_ok), dialog); + gtk_box_pack_end (button_box, widget, FALSE, FALSE, 5); + dialog->priv->ok_button = widget; + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_link_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_LINK_DIALOG, + "editor", editor, + "icon-name", "insert-link", + "title", N_("Link Properties"), + NULL)); +} diff --git a/e-util/e-html-editor-link-dialog.h b/e-util/e-html-editor-link-dialog.h new file mode 100644 index 0000000000..a1e426fe01 --- /dev/null +++ b/e-util/e-html-editor-link-dialog.h @@ -0,0 +1,70 @@ +/* + * e-html-editor-link-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_LINK_DIALOG_H +#define E_HTML_EDITOR_LINK_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_LINK_DIALOG \ + (e_html_editor_link_dialog_get_type ()) +#define E_HTML_EDITOR_LINK_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_LINK_DIALOG, EHTMLEditorLinkDialog)) +#define E_HTML_EDITOR_LINK_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_LINK_DIALOG, EHTMLEditorLinkDialogClass)) +#define E_IS_HTML_EDITOR_LINK_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_LINK_DIALOG)) +#define E_IS_HTML_EDITOR_LINK_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_LINK_DIALOG)) +#define E_HTML_EDITOR_LINK_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_LINK_DIALOG, EHTMLEditorLinkDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorLinkDialog EHTMLEditorLinkDialog; +typedef struct _EHTMLEditorLinkDialogClass EHTMLEditorLinkDialogClass; +typedef struct _EHTMLEditorLinkDialogPrivate EHTMLEditorLinkDialogPrivate; + +struct _EHTMLEditorLinkDialog { + EHTMLEditorDialog parent; + EHTMLEditorLinkDialogPrivate *priv; +}; + +struct _EHTMLEditorLinkDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_link_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_link_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_LINK_DIALOG_H */ diff --git a/e-util/e-html-editor-manager.ui b/e-util/e-html-editor-manager.ui new file mode 100644 index 0000000000..75f2c34b20 --- /dev/null +++ b/e-util/e-html-editor-manager.ui @@ -0,0 +1,181 @@ +<ui> + <menubar name='main-menu'> + <placeholder name='pre-file-menu'/> + <menu action='file-menu'/> + <placeholder name='pre-edit-menu'/> + <menu action='edit-menu'> + <placeholder name='edit-menu-top'/> + <menuitem action='undo'/> + <menuitem action='redo'/> + <separator/> + <menuitem action='cut'/> + <menuitem action='copy'/> + <menuitem action='paste'/> + <menuitem action='paste-quote'/> + <separator/> + <menuitem action='select-all'/> + <separator/> + <menuitem action='show-find'/> + <menuitem action='find-again'/> + <menuitem action='show-replace'/> + <separator/> + <placeholder name='pre-spell-check'/> + <menuitem action='spell-check'/> + <menu action='language-menu'/> + </menu> + <placeholder name='pre-insert-menu'> + <menu action='view-menu'> + <placeholder name='view-menu-top'/> + <menuitem action='webkit-inspector'/> + <separator/> + </menu> + </placeholder> + <menu action='insert-menu'> + <placeholder name='insert-menu-top'/> + <menuitem action='insert-image'/> + <menuitem action='insert-link'/> + <menuitem action='insert-rule'/> + <menuitem action='insert-table'/> + <menuitem action='insert-text-file'/> + <menuitem action='insert-html-file'/> + <menuitem action='insert-emoticon'/> + </menu> + <placeholder name='pre-format-menu'/> + <menu action='format-menu'> + <placeholder name='format-menu-top'/> + <menuitem action='mode-html'/> + <menuitem action='mode-plain'/> + <separator/> + <menu action='font-style-menu'> + <menuitem action='monospaced'/> + <separator/> + <menuitem action='bold'/> + <menuitem action='italic'/> + <menuitem action='underline'/> + <menuitem action='strikethrough'/> + </menu> + <menu action='font-size-menu'> + <menuitem action='size-minus-two'/> + <menuitem action='size-minus-one'/> + <menuitem action='size-plus-zero'/> + <menuitem action='size-plus-one'/> + <menuitem action='size-plus-two'/> + <menuitem action='size-plus-three'/> + <menuitem action='size-plus-four'/> + </menu> + <separator/> + <menu action='paragraph-style-menu'> + <menuitem action='style-normal'/> + <separator/> + <menuitem action='style-h1'/> + <menuitem action='style-h2'/> + <menuitem action='style-h3'/> + <menuitem action='style-h4'/> + <menuitem action='style-h5'/> + <menuitem action='style-h6'/> + <separator/> + <menuitem action='style-list-bullet'/> + <menuitem action='style-list-roman'/> + <menuitem action='style-list-number'/> + <menuitem action='style-list-alpha'/> + <separator/> + <menuitem action='style-address'/> + <menuitem action='style-preformat'/> + </menu> + <menu action='justify-menu'> + <menuitem action='justify-left'/> + <menuitem action='justify-center'/> + <menuitem action='justify-right'/> + </menu> + <separator/> + <menuitem action='indent'/> + <menuitem action='unindent'/> + <menuitem action='wrap-lines'/> + <separator/> + <menuitem action='properties-page'/> + </menu> + </menubar> + <toolbar name='main-toolbar'> + <placeholder name='pre-main-toolbar'/> + <toolitem action='undo'/> + <toolitem action='redo'/> + <separator/> + <toolitem action='cut'/> + <toolitem action='copy'/> + <toolitem action='paste'/> + <separator/> + <toolitem action='show-find'/> + <toolitem action='show-replace'/> + </toolbar> + <toolbar name='edit-toolbar'> + <separator/> + <toolitem action='justify-left'/> + <toolitem action='justify-center'/> + <toolitem action='justify-right'/> + <separator/> + <toolitem action='unindent'/> + <toolitem action='indent'/> + </toolbar> + <toolbar name='html-toolbar'> + <separator/> + <toolitem action='monospaced'/> + <toolitem action='bold'/> + <toolitem action='italic'/> + <toolitem action='underline'/> + <toolitem action='strikethrough'/> + <separator/> + <toolitem action='insert-image'/> + <toolitem action='insert-link'/> + <toolitem action='insert-rule'/> + <toolitem action='insert-table'/> + <toolitem action='insert-emoticon'/> + </toolbar> + <popup name='context-menu'> + <placeholder name='context-spell-suggest'/> + <menu action='context-more-suggestions-menu'/> + <separator/> + <menuitem action='context-spell-ignore'/> + <menu action='context-spell-add-menu'/> + <menuitem action='context-spell-add'/> + <separator/> + <menuitem action='undo'/> + <menuitem action='redo'/> + <separator/> + <menuitem action='cut'/> + <menuitem action='copy'/> + <menuitem action='paste'/> + <menuitem action='paste-quote'/> + <separator/> + <menuitem action='context-insert-link'/> + <menuitem action='context-remove-link'/> + <separator/> + <menu action='context-properties-menu'> + <menuitem action='context-properties-text'/> + <menuitem action='context-properties-link'/> + <menuitem action='context-properties-rule'/> + <menuitem action='context-properties-image'/> + <menuitem action='context-properties-paragraph'/> + <menuitem action='context-properties-cell'/> + <menuitem action='context-properties-table'/> + <menuitem action='context-properties-page'/> + </menu> + <separator/> + <menu action='context-insert-table-menu'> + <menuitem action='context-insert-table'/> + <separator/> + <menuitem action='context-insert-row-above'/> + <menuitem action='context-insert-row-below'/> + <separator/> + <menuitem action='context-insert-column-before'/> + <menuitem action='context-insert-column-after'/> + </menu> + <menu action='context-delete-table-menu'> + <menuitem action='context-delete-table'/> + <menuitem action='context-delete-row'/> + <menuitem action='context-delete-column'/> + <menuitem action='context-delete-cell'/> + </menu> + <separator/> + <menu action='context-input-methods-menu'/> + </popup> +</ui> diff --git a/e-util/e-html-editor-page-dialog.c b/e-util/e-html-editor-page-dialog.c new file mode 100644 index 0000000000..53c8fce8c7 --- /dev/null +++ b/e-util/e-html-editor-page-dialog.c @@ -0,0 +1,513 @@ +/* + * e-html-editor-page-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-page-dialog.h" + +#include <glib/gi18n-lib.h> + +#include "e-color-combo.h" +#include "e-misc-utils.h" + +#define E_HTML_EDITOR_PAGE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_PAGE_DIALOG, EHTMLEditorPageDialogPrivate)) + +struct _EHTMLEditorPageDialogPrivate { + GtkWidget *text_color_picker; + GtkWidget *link_color_picker; + GtkWidget *background_color_picker; + + GtkWidget *background_template_combo; + GtkWidget *background_image_filechooser; +}; + +typedef struct _Template { + const gchar *name; + const gchar *filename; + GdkRGBA text_color; + GdkRGBA link_color; + GdkRGBA background_color; + gint left_margin; +} Template; + +static const Template templates[] = { + + { + N_("None"), + NULL, + { 0.0, 0.0 , 0.0 , 1 }, + { 0.3, 0.55, 0.85, 1 }, + { 1.0, 1.0 , 1.0 , 1 }, + 0 + }, + + { + N_("Perforated Paper"), + "paper.png", + { 0.0, 0.0, 0.0, 1 }, + { 0.0, 0.2, 0.4, 1 }, + { 1.0, 1.0, 1.0, 0 }, + 30 + }, + + { + N_("Blue Ink"), + "texture.png", + { 0.1, 0.12, 0.56, 1 }, + { 0.0, 0.0, 1.0, 1 }, + { 1.0, 1.0, 1.0, 1 }, + 0 + }, + + { + N_("Paper"), + "rect.png", + { 0, 0, 0, 1 }, + { 0, 0, 1, 1 }, + { 1, 1, 1, 1 }, + 0 + }, + + { + N_("Ribbon"), + "ribbon.jpg", + { 0.0, 0.0, 0.0, 1 }, + { 0.6, 0.2, 0.4, 1 }, + { 1.0, 1.0, 1.0, 1 }, + 70 + }, + + { + N_("Midnight"), + "midnight-stars.jpg", + { 1.0, 1.0, 1.0, 1 }, + { 1.0, 0.6, 0.0, 1 }, + { 0.0, 0.0, 0.0, 1 }, + 0 + }, + + { + N_("Confidential"), + "confidential-stamp.jpg", + { 0.0, 0.0, 0.0, 1 }, + { 0.0, 0.0, 1.0, 1 }, + { 1.0, 1.0, 1.0, 1 }, + 0 + }, + + { + N_("Draft"), + "draft-stamp.jpg", + { 0.0, 0.0, 0.0, 1 }, + { 0.0, 0.0, 1.0, 1 }, + { 1.0, 1.0, 1.0, 1 }, + 0 + }, + + { + N_("Graph Paper"), + "draft-paper.png", + { 0.0 , 0.0 , 0.5, 1 }, + { 0.88, 0.13, 0.14, 1 }, + { 1.0 , 1.0 , 1.0 , 1 }, + 0 + } +}; + +G_DEFINE_TYPE ( + EHTMLEditorPageDialog, + e_html_editor_page_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +html_editor_page_dialog_set_text_color (EHTMLEditorPageDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + GdkRGBA rgba; + gchar *color; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->text_color_picker), &rgba); + + color = g_strdup_printf ("#%06x", e_rgba_to_value (&rgba)); + webkit_dom_html_body_element_set_text ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body), color); + + g_free (color); +} + +static void +html_editor_page_dialog_set_link_color (EHTMLEditorPageDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + GdkRGBA rgba; + gchar *color; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->link_color_picker), &rgba); + + color = g_strdup_printf ("#%06x", e_rgba_to_value (&rgba)); + webkit_dom_html_body_element_set_link ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body), color); + + g_free (color); +} + +static void +html_editor_page_dialog_set_background_color (EHTMLEditorPageDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + GdkRGBA rgba; + gchar *color; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), &rgba); + + color = g_strdup_printf ("#%06x", e_rgba_to_value (&rgba)); + + webkit_dom_html_body_element_set_bg_color ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body), color); + + g_free (color); +} + +static void +html_editor_page_dialog_set_background_from_template (EHTMLEditorPageDialog *dialog) +{ + const Template *tmplt; + + tmplt = &templates[ + gtk_combo_box_get_active ( + GTK_COMBO_BOX (dialog->priv->background_template_combo))]; + + /* Special case - 'none' template */ + if (tmplt->filename == NULL) { + gtk_file_chooser_unselect_all ( + GTK_FILE_CHOOSER (dialog->priv->background_image_filechooser)); + } else { + gchar *filename; + + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->text_color_picker), + &tmplt->text_color); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), + &tmplt->background_color); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->link_color_picker), + &tmplt->link_color); + + filename = g_build_filename (EVOLUTION_IMAGESDIR, tmplt->filename, NULL); + + gtk_file_chooser_set_filename ( + GTK_FILE_CHOOSER (dialog->priv->background_image_filechooser), + filename); + g_free (filename); + } + +} + +static void +html_editor_page_dialog_set_background_image (EHTMLEditorPageDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + gchar *uri; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + uri = gtk_file_chooser_get_uri ( + GTK_FILE_CHOOSER ( + dialog->priv->background_image_filechooser)); + + webkit_dom_html_body_element_set_background ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body), uri ? uri : ""); + + g_free (uri); +} + +static void +html_editor_page_dialog_show (GtkWidget *widget) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorPageDialog *dialog; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + gchar *tmp; + GdkRGBA rgba; + + dialog = E_HTML_EDITOR_PAGE_DIALOG (widget); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + tmp = webkit_dom_html_body_element_get_background ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body)); + if (tmp && *tmp) { + gint ii; + gchar *fname = g_filename_from_uri (tmp, NULL, NULL); + for (ii = 0; ii < G_N_ELEMENTS (templates); ii++) { + const Template *tmplt = &templates[ii]; + + if (g_strcmp0 (tmplt->filename, fname) == 0) { + gtk_combo_box_set_active ( + GTK_COMBO_BOX (dialog->priv->background_template_combo), + ii); + break; + } + } + g_free (fname); + } else { + gtk_combo_box_set_active ( + GTK_COMBO_BOX (dialog->priv->background_template_combo), 0); + } + g_free (tmp); + + tmp = webkit_dom_html_body_element_get_text ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body)); + if (!tmp || !*tmp) { + GdkColor *color; + GtkStyle *style = gtk_widget_get_style (GTK_WIDGET (view)); + color = &style->text[GTK_STATE_NORMAL]; + + rgba.alpha = 1; + rgba.red = ((gdouble) color->red) / G_MAXUINT16; + rgba.green = ((gdouble) color->green) / G_MAXUINT16; + rgba.blue = ((gdouble) color->blue) / G_MAXUINT16; + } else { + gdk_rgba_parse (&rgba, tmp); + } + g_free (tmp); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->text_color_picker), &rgba); + + tmp = webkit_dom_html_body_element_get_link ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body)); + if (!tmp || !*tmp) { + GdkColor color; + gtk_widget_style_get ( + GTK_WIDGET (view), "link-color", &color, NULL); + + rgba.alpha = 1; + rgba.red = ((gdouble) color.red) / G_MAXUINT16; + rgba.green = ((gdouble) color.green) / G_MAXUINT16; + rgba.blue = ((gdouble) color.blue) / G_MAXUINT16; + } else { + gdk_rgba_parse (&rgba, tmp); + } + g_free (tmp); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->link_color_picker), &rgba); + + tmp = webkit_dom_html_body_element_get_bg_color ( + WEBKIT_DOM_HTML_BODY_ELEMENT (body)); + if (!tmp || !*tmp) { + GdkColor *color; + GtkStyle *style = gtk_widget_get_style (GTK_WIDGET (view)); + color = &style->base[GTK_STATE_NORMAL]; + + rgba.alpha = 1; + rgba.red = ((gdouble) color->red) / G_MAXUINT16; + rgba.green = ((gdouble) color->green) / G_MAXUINT16; + rgba.blue = ((gdouble) color->blue) / G_MAXUINT16; + + } else { + gdk_rgba_parse (&rgba, tmp); + } + g_free (tmp); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_picker), &rgba); + + GTK_WIDGET_CLASS (e_html_editor_page_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_page_dialog_class_init (EHTMLEditorPageDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorPageDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_page_dialog_show; +} + +static void +e_html_editor_page_dialog_init (EHTMLEditorPageDialog *dialog) +{ + GtkGrid *grid, *main_layout; + GtkWidget *widget; + gint ii; + + dialog->priv = E_HTML_EDITOR_PAGE_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == Colors == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Colors</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Text */ + widget = e_color_combo_new (); + gtk_widget_set_hexpand (widget, TRUE); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_page_dialog_set_text_color), dialog); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->text_color_picker = widget; + + widget = gtk_label_new_with_mnemonic (_("_Text:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->text_color_picker); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Link */ + widget = e_color_combo_new (); + gtk_widget_set_hexpand (widget, TRUE); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_page_dialog_set_link_color), dialog); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + dialog->priv->link_color_picker = widget; + + widget = gtk_label_new_with_mnemonic (_("_Link:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->link_color_picker); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + /* Background */ + widget = e_color_combo_new (); + gtk_widget_set_hexpand (widget, TRUE); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_page_dialog_set_background_color), dialog); + gtk_grid_attach (grid, widget, 1, 2, 1, 1); + dialog->priv->background_color_picker = widget; + + widget = gtk_label_new_with_mnemonic (_("_Background:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_color_picker); + gtk_grid_attach (grid, widget, 0, 2, 1, 1); + + /* == Background Image == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Background Image</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Template */ + widget = gtk_combo_box_text_new (); + for (ii = 0; ii < G_N_ELEMENTS (templates); ii++) { + gtk_combo_box_text_append_text ( + GTK_COMBO_BOX_TEXT (widget), templates[ii].name); + } + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_page_dialog_set_background_from_template), dialog); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->background_template_combo = widget; + + widget = gtk_label_new_with_mnemonic (_("_Template:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_template_combo); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Custom image */ + widget = gtk_file_chooser_button_new ( + _("Selection a file"), GTK_FILE_CHOOSER_ACTION_OPEN); + g_signal_connect_swapped ( + widget, "selection-changed", + G_CALLBACK (html_editor_page_dialog_set_background_image), dialog); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + dialog->priv->background_image_filechooser = widget; + + widget = gtk_label_new_with_mnemonic (_("_Custom:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_image_filechooser); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_page_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_PAGE_DIALOG, + "editor", editor, + "title", N_("Page Properties"), + NULL)); +} diff --git a/e-util/e-html-editor-page-dialog.h b/e-util/e-html-editor-page-dialog.h new file mode 100644 index 0000000000..5678efc4b6 --- /dev/null +++ b/e-util/e-html-editor-page-dialog.h @@ -0,0 +1,70 @@ +/* + * e-html-editor-page-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_PAGE_DIALOG_H +#define E_HTML_EDITOR_PAGE_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_PAGE_DIALOG \ + (e_html_editor_page_dialog_get_type ()) +#define E_HTML_EDITOR_PAGE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_PAGE_DIALOG, EHTMLEditorPageDialog)) +#define E_HTML_EDITOR_PAGE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_PAGE_DIALOG, EHTMLEditorPageDialogClass)) +#define E_IS_HTML_EDITOR_PAGE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_PAGE_DIALOG)) +#define E_IS_HTML_EDITOR_PAGE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_PAGE_DIALOG)) +#define E_HTML_EDITOR_PAGE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_PAGE_DIALOG, EHTMLEditorPageDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorPageDialog EHTMLEditorPageDialog; +typedef struct _EHTMLEditorPageDialogClass EHTMLEditorPageDialogClass; +typedef struct _EHTMLEditorPageDialogPrivate EHTMLEditorPageDialogPrivate; + +struct _EHTMLEditorPageDialog { + EHTMLEditorDialog parent; + EHTMLEditorPageDialogPrivate *priv; +}; + +struct _EHTMLEditorPageDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_page_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_page_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_PAGE_DIALOG_H */ diff --git a/e-util/e-html-editor-paragraph-dialog.c b/e-util/e-html-editor-paragraph-dialog.c new file mode 100644 index 0000000000..f0fce973a6 --- /dev/null +++ b/e-util/e-html-editor-paragraph-dialog.c @@ -0,0 +1,154 @@ +/* + * e-html-editor-paragraph-dialog.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-paragraph-dialog.h" + +#include <glib/gi18n-lib.h> + +#include "e-action-combo-box.h" + +#define E_HTML_EDITOR_PARAGRAPH_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG, EHTMLEditorParagraphDialogPrivate)) + +G_DEFINE_TYPE ( + EHTMLEditorParagraphDialog, + e_html_editor_paragraph_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +struct _EHTMLEditorParagraphDialogPrivate { + GtkWidget *style_combo; + + GtkWidget *left_button; + GtkWidget *center_button; + GtkWidget *right_button; +}; + +static void +html_editor_paragraph_dialog_constructed (GObject *object) +{ + GtkGrid *main_layout, *grid; + GtkWidget *widget; + EHTMLEditor *editor; + EHTMLEditorParagraphDialog *dialog; + + dialog = E_HTML_EDITOR_PARAGRAPH_DIALOG (object); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == General == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>General</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Style */ + widget = e_action_combo_box_new_with_action ( + GTK_RADIO_ACTION (e_html_editor_get_action (editor, "style-normal"))); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + dialog->priv->style_combo = widget; + + widget = gtk_label_new_with_mnemonic (_("_Style:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->style_combo); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* == Alignment == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Alignment</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Left */ + widget = gtk_toggle_button_new_with_label (GTK_STOCK_JUSTIFY_LEFT); + gtk_button_set_use_stock (GTK_BUTTON (widget), TRUE); + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (widget), + e_html_editor_get_action (editor, "justify-left")); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + dialog->priv->left_button = widget; + + /* Center */ + widget = gtk_toggle_button_new_with_label (GTK_STOCK_JUSTIFY_CENTER); + gtk_button_set_use_stock (GTK_BUTTON (widget), TRUE); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (widget), + e_html_editor_get_action (editor, "justify-center")); + dialog->priv->center_button = widget; + + /* Right */ + widget = gtk_toggle_button_new_with_label (GTK_STOCK_JUSTIFY_RIGHT); + gtk_button_set_use_stock (GTK_BUTTON (widget), TRUE); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + gtk_activatable_set_related_action ( + GTK_ACTIVATABLE (widget), + e_html_editor_get_action (editor, "justify-right")); + dialog->priv->right_button = widget; + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +static void +e_html_editor_paragraph_dialog_class_init (EHTMLEditorParagraphDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EHTMLEditorParagraphDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = html_editor_paragraph_dialog_constructed; +} + +static void +e_html_editor_paragraph_dialog_init (EHTMLEditorParagraphDialog *dialog) +{ + dialog->priv = E_HTML_EDITOR_PARAGRAPH_DIALOG_GET_PRIVATE (dialog); +} + +GtkWidget * +e_html_editor_paragraph_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG, + "editor", editor, + "title", N_("Paragraph Properties"), + NULL)); +} diff --git a/e-util/e-html-editor-paragraph-dialog.h b/e-util/e-html-editor-paragraph-dialog.h new file mode 100644 index 0000000000..17855bd3c8 --- /dev/null +++ b/e-util/e-html-editor-paragraph-dialog.h @@ -0,0 +1,71 @@ +/* + * e-html-editor-paragraph-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_PARAGRAPH_DIALOG_H +#define E_HTML_EDITOR_PARAGRAPH_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG \ + (e_html_editor_paragraph_dialog_get_type ()) +#define E_HTML_EDITOR_PARAGRAPH_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG, EHTMLEditorParagraphDialog)) +#define E_HTML_EDITOR_PARAGRAPH_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG, EHTMLEditorParagraphDialogClass)) +#define E_IS_HTML_EDITOR_PARAGRAPH_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG)) +#define E_IS_HTML_EDITOR_PARAGRAPH_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG)) +#define E_HTML_EDITOR_PARAGRAPH_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_PARAGRAPH_DIALOG, EHTMLEditorParagraphDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorParagraphDialog EHTMLEditorParagraphDialog; +typedef struct _EHTMLEditorParagraphDialogClass EHTMLEditorParagraphDialogClass; +typedef struct _EHTMLEditorParagraphDialogPrivate EHTMLEditorParagraphDialogPrivate; + +struct _EHTMLEditorParagraphDialog { + EHTMLEditorDialog parent; + EHTMLEditorParagraphDialogPrivate *priv; +}; + +struct _EHTMLEditorParagraphDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_paragraph_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_paragraph_dialog_new + (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_PARAGRAPH_DIALOG_H */ diff --git a/e-util/e-html-editor-private.h b/e-util/e-html-editor-private.h new file mode 100644 index 0000000000..dc4658bd57 --- /dev/null +++ b/e-util/e-html-editor-private.h @@ -0,0 +1,103 @@ +/* + * e-html-editor-private.h + * + * 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. + */ + +#ifndef E_HTML_EDITOR_PRIVATE_H +#define E_HTML_EDITOR_PRIVATE_H + +#include <e-action-combo-box.h> +#include <e-color-combo.h> +#include <e-html-editor.h> +#include <e-html-editor-actions.h> +#include <e-html-editor-cell-dialog.h> +#include <e-html-editor-find-dialog.h> +#include <e-html-editor-hrule-dialog.h> +#include <e-html-editor-image-dialog.h> +#include <e-html-editor-link-dialog.h> +#include <e-html-editor-page-dialog.h> +#include <e-html-editor-paragraph-dialog.h> +#include <e-html-editor-replace-dialog.h> +#include <e-html-editor-spell-check-dialog.h> +#include <e-html-editor-table-dialog.h> +#include <e-html-editor-text-dialog.h> +#include <e-html-editor-view.h> + +#ifdef HAVE_XFREE +#include <X11/XF86keysym.h> +#endif + +#define ACTION(name) (E_HTML_EDITOR_ACTION_##name (editor)) +#define WIDGET(name) (E_HTML_EDITOR_WIDGETS_##name (editor)) + +G_BEGIN_DECLS + +struct _EHTMLEditorPrivate { + GtkUIManager *manager; + GtkActionGroup *core_actions; + GtkActionGroup *html_actions; + GtkActionGroup *context_actions; + GtkActionGroup *html_context_actions; + GtkActionGroup *language_actions; + GtkActionGroup *spell_check_actions; + GtkActionGroup *suggestion_actions; + + GtkWidget *main_menu; + GtkWidget *main_toolbar; + GtkWidget *edit_toolbar; + GtkWidget *html_toolbar; + GtkWidget *activity_bar; + GtkWidget *alert_bar; + GtkWidget *edit_area; + + GtkWidget *find_dialog; + GtkWidget *replace_dialog; + GtkWidget *link_dialog; + GtkWidget *hrule_dialog; + GtkWidget *table_dialog; + GtkWidget *page_dialog; + GtkWidget *image_dialog; + GtkWidget *text_dialog; + GtkWidget *paragraph_dialog; + GtkWidget *cell_dialog; + GtkWidget *spell_check_dialog; + + GtkWidget *color_combo_box; + GtkWidget *mode_combo_box; + GtkWidget *size_combo_box; + GtkWidget *style_combo_box; + GtkWidget *scrolled_window; + + EHTMLEditorView *html_editor_view; + EHTMLEditorSelection *selection; + + gchar *filename; + + guint spell_suggestions_merge_id; + + WebKitDOMNode *image; + WebKitDOMNode *table_cell; + + gint editor_layout_row; +}; + +void editor_actions_init (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_PRIVATE_H */ diff --git a/e-util/e-html-editor-replace-dialog.c b/e-util/e-html-editor-replace-dialog.c new file mode 100644 index 0000000000..7addcdfdad --- /dev/null +++ b/e-util/e-html-editor-replace-dialog.c @@ -0,0 +1,288 @@ +/* + * e-html-editor-replace-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-replace-dialog.h" + +#include <glib/gi18n-lib.h> + +#define E_HTML_EDITOR_REPLACE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_REPLACE_DIALOG, EHTMLEditorReplaceDialogPrivate)) + +G_DEFINE_TYPE ( + EHTMLEditorReplaceDialog, + e_html_editor_replace_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +struct _EHTMLEditorReplaceDialogPrivate { + GtkWidget *search_entry; + GtkWidget *replace_entry; + + GtkWidget *case_sensitive; + GtkWidget *backwards; + GtkWidget *wrap; + + GtkWidget *result_label; + + GtkWidget *skip_button; + GtkWidget *replace_button; + GtkWidget *replace_all_button; + + EHTMLEditor *editor; +}; + +enum { + PROP_0, + PROP_EDITOR +}; + +static gboolean +jump (EHTMLEditorReplaceDialog *dialog) +{ + EHTMLEditor *editor; + WebKitWebView *webview; + gboolean found; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + webview = WEBKIT_WEB_VIEW ( + e_html_editor_get_view (editor)); + + found = webkit_web_view_search_text ( + webview, + gtk_entry_get_text (GTK_ENTRY (dialog->priv->search_entry)), + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->case_sensitive)), + !gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->backwards)), + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->wrap))); + + return found; +} + +static void +html_editor_replace_dialog_skip_cb (EHTMLEditorReplaceDialog *dialog) +{ + if (!jump (dialog)) { + gtk_label_set_label ( + GTK_LABEL (dialog->priv->result_label), + N_("No match found")); + gtk_widget_show (dialog->priv->result_label); + } else { + gtk_widget_hide (dialog->priv->result_label); + } +} + +static void +html_editor_replace_dialog_replace_cb (EHTMLEditorReplaceDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + /* Jump to next matching word */ + if (!jump (dialog)) { + gtk_label_set_label ( + GTK_LABEL (dialog->priv->result_label), + N_("No match found")); + gtk_widget_show (dialog->priv->result_label); + return; + } else { + gtk_widget_hide (dialog->priv->result_label); + } + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_replace ( + selection, + gtk_entry_get_text (GTK_ENTRY (dialog->priv->replace_entry))); +} + +static void +html_editor_replace_dialog_replace_all_cb (EHTMLEditorReplaceDialog *dialog) +{ + gint i = 0; + gchar *result; + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + const gchar *replacement; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + replacement = gtk_entry_get_text (GTK_ENTRY (dialog->priv->replace_entry)); + + while (jump (dialog)) { + e_html_editor_selection_replace (selection, replacement); + i++; + + /* Jump behind the word */ + e_html_editor_selection_move ( + selection, TRUE, E_HTML_EDITOR_SELECTION_GRANULARITY_WORD); + } + + result = g_strdup_printf (_("%d occurences replaced"), i); + gtk_label_set_label (GTK_LABEL (dialog->priv->result_label), result); + gtk_widget_show (dialog->priv->result_label); + g_free (result); +} + +static void +html_editor_replace_dialog_entry_changed (EHTMLEditorReplaceDialog *dialog) +{ + gboolean ready; + ready = ((gtk_entry_get_text_length ( + GTK_ENTRY (dialog->priv->search_entry)) != 0) && + (gtk_entry_get_text_length ( + GTK_ENTRY (dialog->priv->replace_entry)) != 0)); + + gtk_widget_set_sensitive (dialog->priv->skip_button, ready); + gtk_widget_set_sensitive (dialog->priv->replace_button, ready); + gtk_widget_set_sensitive (dialog->priv->replace_all_button, ready); +} + +static void +html_editor_replace_dialog_show (GtkWidget *widget) +{ + EHTMLEditorReplaceDialog *dialog = E_HTML_EDITOR_REPLACE_DIALOG (widget); + + gtk_widget_grab_focus (dialog->priv->search_entry); + gtk_widget_hide (dialog->priv->result_label); + + /* Chain up to parent implementation */ + GTK_WIDGET_CLASS (e_html_editor_replace_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_replace_dialog_class_init (EHTMLEditorReplaceDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorReplaceDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_replace_dialog_show; +} + +static void +e_html_editor_replace_dialog_init (EHTMLEditorReplaceDialog *dialog) +{ + GtkGrid *main_layout; + GtkWidget *widget, *layout; + GtkBox *button_box; + + dialog->priv = E_HTML_EDITOR_REPLACE_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + widget = gtk_entry_new (); + gtk_grid_attach (main_layout, widget, 1, 0, 2, 1); + dialog->priv->search_entry = widget; + g_signal_connect_swapped ( + widget, "notify::text-length", + G_CALLBACK (html_editor_replace_dialog_entry_changed), dialog); + + widget = gtk_label_new_with_mnemonic (_("R_eplace:")); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->search_entry); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + widget = gtk_entry_new (); + gtk_grid_attach (main_layout, widget, 1, 1, 2, 1); + dialog->priv->replace_entry = widget; + g_signal_connect_swapped ( + widget, "notify::text-length", + G_CALLBACK (html_editor_replace_dialog_entry_changed), dialog); + + widget = gtk_label_new_with_mnemonic (_("_With:")); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->replace_entry); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_grid_attach (main_layout, widget, 0, 1, 1, 1); + + layout = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); + gtk_grid_attach (main_layout, layout, 1, 2, 2, 1); + + widget = gtk_check_button_new_with_mnemonic (_("Search _backwards")); + gtk_box_pack_start (GTK_BOX (layout), widget, FALSE, FALSE, 0); + dialog->priv->backwards = widget; + + widget = gtk_check_button_new_with_mnemonic (_("_Case sensitive")); + gtk_box_pack_start (GTK_BOX (layout), widget, FALSE, FALSE, 0); + dialog->priv->case_sensitive = widget; + + widget = gtk_check_button_new_with_mnemonic (_("Wra_p search")); + gtk_box_pack_start (GTK_BOX (layout), widget, FALSE, FALSE, 0); + dialog->priv->wrap = widget; + + widget = gtk_label_new (""); + gtk_grid_attach (main_layout, widget, 0, 3, 2, 1); + dialog->priv->result_label = widget; + + button_box = e_html_editor_dialog_get_button_box (E_HTML_EDITOR_DIALOG (dialog)); + + widget = gtk_button_new_with_mnemonic (_("_Skip")); + gtk_box_pack_start (button_box, widget, FALSE, FALSE, 5); + gtk_widget_set_sensitive (widget, FALSE); + dialog->priv->skip_button = widget; + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_replace_dialog_skip_cb), dialog); + + widget = gtk_button_new_with_mnemonic (_("_Replace")); + gtk_box_pack_start (button_box, widget, FALSE, FALSE, 5); + gtk_widget_set_sensitive (widget, FALSE); + dialog->priv->replace_button = widget; + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_replace_dialog_replace_cb), dialog); + + widget = gtk_button_new_with_mnemonic (_("Replace _All")); + gtk_box_pack_start (button_box, widget, FALSE, FALSE, 5); + gtk_widget_set_sensitive (widget, FALSE); + dialog->priv->replace_all_button = widget; + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_replace_dialog_replace_all_cb), dialog); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_replace_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_REPLACE_DIALOG, + "editor", editor, + "icon-name", GTK_STOCK_FIND_AND_REPLACE, + "resizable", FALSE, + "title", N_("Replace"), + "transient-for", gtk_widget_get_toplevel (GTK_WIDGET (editor)), + "type", GTK_WINDOW_TOPLEVEL, + "window-position", GTK_WIN_POS_CENTER_ON_PARENT, + NULL)); +} diff --git a/e-util/e-html-editor-replace-dialog.h b/e-util/e-html-editor-replace-dialog.h new file mode 100644 index 0000000000..f253e428aa --- /dev/null +++ b/e-util/e-html-editor-replace-dialog.h @@ -0,0 +1,71 @@ +/* + * e-html-editor-replace-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_REPLACE_DIALOG_H +#define E_HTML_EDITOR_REPLACE_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_REPLACE_DIALOG \ + (e_html_editor_replace_dialog_get_type ()) +#define E_HTML_EDITOR_REPLACE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_REPLACE_DIALOG, EHTMLEditorReplaceDialog)) +#define E_HTML_EDITOR_REPLACE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_REPLACE_DIALOG, EHTMLEditorReplaceDialogClass)) +#define E_IS_HTML_EDITOR_REPLACE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_REPLACE_DIALOG)) +#define E_IS_HTML_EDITOR_REPLACE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_REPLACE_DIALOG)) +#define E_HTML_EDITOR_REPLACE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_REPLACE_DIALOG, EHTMLEditorReplaceDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorReplaceDialog EHTMLEditorReplaceDialog; +typedef struct _EHTMLEditorReplaceDialogClass EHTMLEditorReplaceDialogClass; +typedef struct _EHTMLEditorReplaceDialogPrivate EHTMLEditorReplaceDialogPrivate; + +struct _EHTMLEditorReplaceDialog { + EHTMLEditorDialog parent; + EHTMLEditorReplaceDialogPrivate *priv; +}; + +struct _EHTMLEditorReplaceDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_replace_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_replace_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_REPLACE_DIALOG_H */ + diff --git a/e-util/e-html-editor-selection.c b/e-util/e-html-editor-selection.c new file mode 100644 index 0000000000..c109063f41 --- /dev/null +++ b/e-util/e-html-editor-selection.c @@ -0,0 +1,5576 @@ +/* + * e-html-editor-selection.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-selection.h" +#include "e-html-editor-view.h" +#include "e-html-editor.h" +#include "e-html-editor-utils.h" + +#include <e-util/e-util.h> + +#include <webkit/webkit.h> +#include <webkit/webkitdom.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> + +#define E_HTML_EDITOR_SELECTION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_SELECTION, EHTMLEditorSelectionPrivate)) + +#define UNICODE_ZERO_WIDTH_SPACE "\xe2\x80\x8b" +#define UNICODE_NBSP "\xc2\xa0" + +#define SPACES_PER_INDENTATION 4 +#define SPACES_PER_LIST_LEVEL 8 + +/** + * EHTMLEditorSelection + * + * The #EHTMLEditorSelection object represents current position of the cursor + * with the editor or current text selection within the editor. To obtain + * valid #EHTMLEditorSelection, call e_html_editor_view_get_selection(). + */ + +struct _EHTMLEditorSelectionPrivate { + + GWeakRef html_editor_view; + gulong selection_changed_handler_id; + + gchar *text; + + gboolean is_bold; + gboolean is_italic; + gboolean is_underline; + gboolean is_monospaced; + gboolean is_strikethrough; + + gchar *background_color; + gchar *font_color; + gchar *font_family; + + gulong selection_offset; + + gint word_wrap_length; + guint font_size; + + EHTMLEditorSelectionAlignment alignment; +}; + +enum { + PROP_0, + PROP_ALIGNMENT, + PROP_BACKGROUND_COLOR, + PROP_BLOCK_FORMAT, + PROP_BOLD, + PROP_HTML_EDITOR_VIEW, + PROP_FONT_COLOR, + PROP_FONT_NAME, + PROP_FONT_SIZE, + PROP_INDENTED, + PROP_ITALIC, + PROP_MONOSPACED, + PROP_STRIKETHROUGH, + PROP_SUBSCRIPT, + PROP_SUPERSCRIPT, + PROP_TEXT, + PROP_UNDERLINE +}; + +static const GdkRGBA black = { 0 }; + +G_DEFINE_TYPE ( + EHTMLEditorSelection, + e_html_editor_selection, + G_TYPE_OBJECT +); + +static WebKitDOMRange * +html_editor_selection_get_current_range (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range = NULL; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + document = webkit_web_view_get_dom_document (web_view); + window = webkit_dom_document_get_default_view (document); + if (!window) + goto exit; + + dom_selection = webkit_dom_dom_window_get_selection (window); + if (!WEBKIT_DOM_IS_DOM_SELECTION (dom_selection)) + goto exit; + + if (webkit_dom_dom_selection_get_range_count (dom_selection) < 1) + goto exit; + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + + exit: + g_object_unref (view); + + return range; +} + +static gboolean +get_has_style (EHTMLEditorSelection *selection, + const gchar *style_tag) +{ + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + gboolean result; + gint tag_len; + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_start_container (range, NULL); + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + tag_len = strlen (style_tag); + result = FALSE; + while (!result && element) { + gchar *element_tag; + gboolean accept_citation = FALSE; + + element_tag = webkit_dom_element_get_tag_name (element); + + if (g_ascii_strncasecmp (style_tag, "citation", 8) == 0) { + accept_citation = TRUE; + result = ((strlen (element_tag) == 10 /* strlen ("blockquote") */) && + (g_ascii_strncasecmp (element_tag, "blockquote", 10) == 0)); + if (element_has_class (element, "-x-evo-indented")) + result = FALSE; + } else { + result = ((tag_len == strlen (element_tag)) && + (g_ascii_strncasecmp (element_tag, style_tag, tag_len) == 0)); + } + + /* Special case: <blockquote type=cite> marks quotation, while + * just <blockquote> is used for indentation. If the <blockquote> + * has type=cite, then ignore it unless style_tag is "citation" */ + if (result && g_ascii_strncasecmp (element_tag, "blockquote", 10) == 0) { + if (webkit_dom_element_has_attribute (element, "type")) { + gchar *type; + type = webkit_dom_element_get_attribute (element, "type"); + if (!accept_citation && (g_ascii_strncasecmp (type, "cite", 4) == 0)) { + result = FALSE; + } + g_free (type); + } else { + if (accept_citation) + result = FALSE; + } + } + + g_free (element_tag); + + if (result) + break; + + element = webkit_dom_node_get_parent_element ( + WEBKIT_DOM_NODE (element)); + } + + return result; +} + +static gchar * +get_font_property (EHTMLEditorSelection *selection, + const gchar *font_property) +{ + WebKitDOMRange *range; + WebKitDOMNode *node; + WebKitDOMElement *element; + gchar *value; + + range = html_editor_selection_get_current_range (selection); + if (!range) + return NULL; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + element = e_html_editor_dom_node_find_parent_element (node, "FONT"); + if (!element) + return NULL; + + g_object_get (G_OBJECT (element), font_property, &value, NULL); + + return value; +} + +static void +html_editor_selection_selection_changed_cb (WebKitWebView *webview, + EHTMLEditorSelection *selection) +{ + g_object_freeze_notify (G_OBJECT (selection)); + + g_object_notify (G_OBJECT (selection), "alignment"); + g_object_notify (G_OBJECT (selection), "background-color"); + g_object_notify (G_OBJECT (selection), "bold"); + g_object_notify (G_OBJECT (selection), "font-name"); + g_object_notify (G_OBJECT (selection), "font-size"); + g_object_notify (G_OBJECT (selection), "font-color"); + g_object_notify (G_OBJECT (selection), "block-format"); + g_object_notify (G_OBJECT (selection), "indented"); + g_object_notify (G_OBJECT (selection), "italic"); + g_object_notify (G_OBJECT (selection), "monospaced"); + g_object_notify (G_OBJECT (selection), "strikethrough"); + g_object_notify (G_OBJECT (selection), "subscript"); + g_object_notify (G_OBJECT (selection), "superscript"); + g_object_notify (G_OBJECT (selection), "text"); + g_object_notify (G_OBJECT (selection), "underline"); + + g_object_thaw_notify (G_OBJECT (selection)); +} + +void +e_html_editor_selection_block_selection_changed (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_signal_handlers_block_by_func ( + view, html_editor_selection_selection_changed_cb, selection); + g_object_unref (view); +} + +void +e_html_editor_selection_unblock_selection_changed (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_signal_handlers_unblock_by_func ( + view, html_editor_selection_selection_changed_cb, selection); + g_object_unref (view); +} + +static void +html_editor_selection_set_html_editor_view (EHTMLEditorSelection *selection, + EHTMLEditorView *view) +{ + gulong handler_id; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + g_weak_ref_set (&selection->priv->html_editor_view, view); + + handler_id = g_signal_connect ( + view, "selection-changed", + G_CALLBACK (html_editor_selection_selection_changed_cb), + selection); + + selection->priv->selection_changed_handler_id = handler_id; +} + +static void +html_editor_selection_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GdkRGBA rgba = { 0 }; + + switch (property_id) { + case PROP_ALIGNMENT: + g_value_set_int ( + value, + e_html_editor_selection_get_alignment ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_BACKGROUND_COLOR: + g_value_set_string ( + value, + e_html_editor_selection_get_background_color ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_BLOCK_FORMAT: + g_value_set_int ( + value, + e_html_editor_selection_get_block_format ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_BOLD: + g_value_set_boolean ( + value, + e_html_editor_selection_is_bold ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_HTML_EDITOR_VIEW: + g_value_take_object ( + value, + e_html_editor_selection_ref_html_editor_view ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_FONT_COLOR: + e_html_editor_selection_get_font_color ( + E_HTML_EDITOR_SELECTION (object), &rgba); + g_value_set_boxed (value, &rgba); + return; + + case PROP_FONT_NAME: + g_value_set_string ( + value, + e_html_editor_selection_get_font_name ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_FONT_SIZE: + g_value_set_int ( + value, + e_html_editor_selection_get_font_size ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_INDENTED: + g_value_set_boolean ( + value, + e_html_editor_selection_is_indented ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_ITALIC: + g_value_set_boolean ( + value, + e_html_editor_selection_is_italic ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_MONOSPACED: + g_value_set_boolean ( + value, + e_html_editor_selection_is_monospaced ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_STRIKETHROUGH: + g_value_set_boolean ( + value, + e_html_editor_selection_is_strikethrough ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_SUBSCRIPT: + g_value_set_boolean ( + value, + e_html_editor_selection_is_subscript ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_SUPERSCRIPT: + g_value_set_boolean ( + value, + e_html_editor_selection_is_superscript ( + E_HTML_EDITOR_SELECTION (object))); + return; + + case PROP_TEXT: + g_value_set_string ( + value, + e_html_editor_selection_get_string ( + E_HTML_EDITOR_SELECTION (object))); + break; + + case PROP_UNDERLINE: + g_value_set_boolean ( + value, + e_html_editor_selection_is_underline ( + E_HTML_EDITOR_SELECTION (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_selection_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALIGNMENT: + e_html_editor_selection_set_alignment ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_int (value)); + return; + + case PROP_BACKGROUND_COLOR: + e_html_editor_selection_set_background_color ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_string (value)); + return; + + case PROP_BOLD: + e_html_editor_selection_set_bold ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_HTML_EDITOR_VIEW: + html_editor_selection_set_html_editor_view ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_object (value)); + return; + + case PROP_FONT_COLOR: + e_html_editor_selection_set_font_color ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boxed (value)); + return; + + case PROP_BLOCK_FORMAT: + e_html_editor_selection_set_block_format ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_int (value)); + return; + + case PROP_FONT_NAME: + e_html_editor_selection_set_font_name ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_string (value)); + return; + + case PROP_FONT_SIZE: + e_html_editor_selection_set_font_size ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_int (value)); + return; + + case PROP_ITALIC: + e_html_editor_selection_set_italic ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_MONOSPACED: + e_html_editor_selection_set_monospaced ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_STRIKETHROUGH: + e_html_editor_selection_set_strikethrough ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_SUBSCRIPT: + e_html_editor_selection_set_subscript ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_SUPERSCRIPT: + e_html_editor_selection_set_superscript ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + + case PROP_UNDERLINE: + e_html_editor_selection_set_underline ( + E_HTML_EDITOR_SELECTION (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_selection_dispose (GObject *object) +{ + EHTMLEditorSelectionPrivate *priv; + EHTMLEditorView *view; + + priv = E_HTML_EDITOR_SELECTION_GET_PRIVATE (object); + + view = g_weak_ref_get (&priv->html_editor_view); + if (view != NULL) { + g_signal_handler_disconnect ( + view, priv->selection_changed_handler_id); + priv->selection_changed_handler_id = 0; + g_object_unref (view); + } + + g_weak_ref_set (&priv->html_editor_view, NULL); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_html_editor_selection_parent_class)->dispose (object); +} + +static void +html_editor_selection_finalize (GObject *object) +{ + EHTMLEditorSelection *selection = E_HTML_EDITOR_SELECTION (object); + + g_free (selection->priv->text); + g_free (selection->priv->background_color); + g_free (selection->priv->font_color); + g_free (selection->priv->font_family); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_html_editor_selection_parent_class)->finalize (object); +} + +static void +e_html_editor_selection_class_init (EHTMLEditorSelectionClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorSelectionPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = html_editor_selection_get_property; + object_class->set_property = html_editor_selection_set_property; + object_class->dispose = html_editor_selection_dispose; + object_class->finalize = html_editor_selection_finalize; + + /** + * EHTMLEditorSelectionalignment + * + * Holds alignment of current paragraph. + */ + /* FIXME: Convert the enum to a proper type */ + g_object_class_install_property ( + object_class, + PROP_ALIGNMENT, + g_param_spec_int ( + "alignment", + NULL, + NULL, + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT, + E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT, + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT, + G_PARAM_READWRITE)); + + /** + * EHTMLEditorSelectionbackground-color + * + * Holds background color of current selection or at current cursor + * position. + */ + g_object_class_install_property ( + object_class, + PROP_BACKGROUND_COLOR, + g_param_spec_string ( + "background-color", + NULL, + NULL, + NULL, + G_PARAM_READWRITE)); + + /** + * EHTMLEditorSelectionblock-format + * + * Holds block format of current paragraph. See + * #EHTMLEditorSelectionBlockFormat for valid values. + */ + /* FIXME Convert the EHTMLEditorSelectionBlockFormat + * enum to a proper type. */ + g_object_class_install_property ( + object_class, + PROP_BLOCK_FORMAT, + g_param_spec_int ( + "block-format", + NULL, + NULL, + 0, + G_MAXINT, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionbold + * + * Holds whether current selection or text at current cursor position + * is bold. + */ + g_object_class_install_property ( + object_class, + PROP_BOLD, + g_param_spec_boolean ( + "bold", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_HTML_EDITOR_VIEW, + g_param_spec_object ( + "html-editor-view", + NULL, + NULL, + E_TYPE_HTML_EDITOR_VIEW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionfont-color + * + * Holds font color of current selection or at current cursor position. + */ + g_object_class_install_property ( + object_class, + PROP_FONT_COLOR, + g_param_spec_boxed ( + "font-color", + NULL, + NULL, + GDK_TYPE_RGBA, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionfont-name + * + * Holds name of font in current selection or at current cursor + * position. + */ + g_object_class_install_property ( + object_class, + PROP_FONT_NAME, + g_param_spec_string ( + "font-name", + NULL, + NULL, + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionfont-size + * + * Holds point size of current selection or at current cursor position. + */ + g_object_class_install_property ( + object_class, + PROP_FONT_SIZE, + g_param_spec_int ( + "font-size", + NULL, + NULL, + 1, + 7, + 3, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionindented + * + * Holds whether current paragraph is indented. This does not include + * citations. + */ + g_object_class_install_property ( + object_class, + PROP_INDENTED, + g_param_spec_boolean ( + "indented", + NULL, + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionitalic + * + * Holds whether current selection or letter at current cursor position + * is italic. + */ + g_object_class_install_property ( + object_class, + PROP_ITALIC, + g_param_spec_boolean ( + "italic", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionmonospaced + * + * Holds whether current selection or letter at current cursor position + * is monospaced. + */ + g_object_class_install_property ( + object_class, + PROP_MONOSPACED, + g_param_spec_boolean ( + "monospaced", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionstrikethrough + * + * Holds whether current selection or letter at current cursor position + * is strikethrough. + */ + g_object_class_install_property ( + object_class, + PROP_STRIKETHROUGH, + g_param_spec_boolean ( + "strikethrough", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionsuperscript + * + * Holds whether current selection or letter at current cursor position + * is in superscript. + */ + g_object_class_install_property ( + object_class, + PROP_SUPERSCRIPT, + g_param_spec_boolean ( + "superscript", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionsubscript + * + * Holds whether current selection or letter at current cursor position + * is in subscript. + */ + g_object_class_install_property ( + object_class, + PROP_SUBSCRIPT, + g_param_spec_boolean ( + "subscript", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectiontext + * + * Holds always up-to-date text of current selection. + */ + g_object_class_install_property ( + object_class, + PROP_TEXT, + g_param_spec_string ( + "text", + NULL, + NULL, + NULL, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorSelectionunderline + * + * Holds whether current selection or letter at current cursor position + * is underlined. + */ + g_object_class_install_property ( + object_class, + PROP_UNDERLINE, + g_param_spec_boolean ( + "underline", + NULL, + NULL, + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_html_editor_selection_init (EHTMLEditorSelection *selection) +{ + GSettings *g_settings; + + selection->priv = E_HTML_EDITOR_SELECTION_GET_PRIVATE (selection); + + g_settings = g_settings_new ("org.gnome.evolution.mail"); + selection->priv->word_wrap_length = + g_settings_get_int (g_settings, "composer-word-wrap-length"); + g_object_unref (g_settings); +} + +gint +e_html_editor_selection_get_word_wrap_length (EHTMLEditorSelection *selection) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), 72); + + return selection->priv->word_wrap_length; +} + +/** + * e_html_editor_selection_ref_html_editor_view: + * @selection: an #EHTMLEditorSelection + * + * Returns a new reference to @selection's #EHTMLEditorView. Unreference + * the #EHTMLEditorView with g_object_unref() when finished with it. + * + * Returns: an #EHTMLEditorView + **/ +EHTMLEditorView * +e_html_editor_selection_ref_html_editor_view (EHTMLEditorSelection *selection) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + return g_weak_ref_get (&selection->priv->html_editor_view); +} + +/** + * e_html_editor_selection_has_text: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection contains any text. + * + * Returns: @TRUE when current selection contains text, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_has_text (EHTMLEditorSelection *selection) +{ + WebKitDOMRange *range; + WebKitDOMNode *node; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + range = html_editor_selection_get_current_range (selection); + + node = webkit_dom_range_get_start_container (range, NULL); + if (webkit_dom_node_get_node_type (node) == 3) + return TRUE; + + node = webkit_dom_range_get_end_container (range, NULL); + if (webkit_dom_node_get_node_type (node) == 3) + return TRUE; + + node = WEBKIT_DOM_NODE (webkit_dom_range_clone_contents (range, NULL)); + while (node) { + if (webkit_dom_node_get_node_type (node) == 3) + return TRUE; + + if (webkit_dom_node_has_child_nodes (node)) { + node = webkit_dom_node_get_first_child (node); + } else if (webkit_dom_node_get_next_sibling (node)) { + node = webkit_dom_node_get_next_sibling (node); + } else { + node = webkit_dom_node_get_parent_node (node); + if (node) { + node = webkit_dom_node_get_next_sibling (node); + } + } + } + + return FALSE; +} + +/** + * e_html_editor_selection_get_caret_word: + * @selection: an #EHTMLEditorSelection + * + * Returns word under cursor. + * + * Returns: A newly allocated string with current caret word or @NULL when there + * is no text under cursor or when selection is active. [transfer-full]. + */ +gchar * +e_html_editor_selection_get_caret_word (EHTMLEditorSelection *selection) +{ + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + range = html_editor_selection_get_current_range (selection); + + /* Don't operate on the visible selection */ + range = webkit_dom_range_clone_range (range, NULL); + webkit_dom_range_expand (range, "word", NULL); + + return webkit_dom_range_to_string (range, NULL); +} + +/** + * e_html_editor_selection_replace_caret_word: + * @selection: an #EHTMLEditorSelection + * @replacement: a string to replace current caret word with + * + * Replaces current word under cursor with @replacement. + */ +void +e_html_editor_selection_replace_caret_word (EHTMLEditorSelection *selection, + const gchar *replacement) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (replacement != NULL); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + range = html_editor_selection_get_current_range (selection); + document = webkit_web_view_get_dom_document (web_view); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + webkit_dom_range_expand (range, "word", NULL); + webkit_dom_dom_selection_add_range (dom_selection, range); + + e_html_editor_selection_insert_html (selection, replacement); + + g_object_unref (view); +} + +/** + * e_html_editor_selection_get_string: + * @selection: an #EHTMLEditorSelection + * + * Returns currently selected string. + * + * Returns: A pointer to content of current selection. The string is owned by + * #EHTMLEditorSelection and should not be free'd. + */ +const gchar * +e_html_editor_selection_get_string (EHTMLEditorSelection *selection) +{ + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return NULL; + + g_free (selection->priv->text); + selection->priv->text = webkit_dom_range_get_text (range); + + return selection->priv->text; +} + +/** + * e_html_editor_selection_replace: + * @selection: an #EHTMLEditorSelection + * @new_string: a string to replace current selection with + * + * Replaces currently selected text with @new_string. + */ +void +e_html_editor_selection_replace (EHTMLEditorSelection *selection, + const gchar *new_string) +{ + EHTMLEditorView *view; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, new_string); + + g_object_unref (view); +} + +/** + * e_html_editor_selection_get_list_alignment_from_node: + * @node: #an WebKitDOMNode + * + * Returns alignment of given list. + * + * Returns: #EHTMLEditorSelectionAlignment + */ +EHTMLEditorSelectionAlignment +e_html_editor_selection_get_list_alignment_from_node (WebKitDOMNode *node) +{ + if (element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-list-item-align-left")) + return E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; + if (element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-list-item-align-center")) + return E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER; + if (element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-list-item-align-right")) + return E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT; + + return E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; +} + +/** + * e_html_editor_selection_get_alignment: + * @selection: #an EHTMLEditorSelection + * + * Returns alignment of current paragraph + * + * Returns: #EHTMLEditorSelectionAlignment + */ +EHTMLEditorSelectionAlignment +e_html_editor_selection_get_alignment (EHTMLEditorSelection *selection) +{ + EHTMLEditorSelectionAlignment alignment; + EHTMLEditorView *view; + gchar *value; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMElement *element; + WebKitDOMNode *node; + WebKitDOMRange *range; + + g_return_val_if_fail ( + E_IS_HTML_EDITOR_SELECTION (selection), + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + range = html_editor_selection_get_current_range (selection); + if (!range) + return E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; + + node = webkit_dom_range_get_start_container (range, NULL); + if (!node) + return E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "text-align"); + + if (!value || !*value || + (g_ascii_strncasecmp (value, "left", 4) == 0)) { + alignment = E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; + } else if (g_ascii_strncasecmp (value, "center", 6) == 0) { + alignment = E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER; + } else if (g_ascii_strncasecmp (value, "right", 5) == 0) { + alignment = E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT; + } else { + alignment = E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT; + } + + g_free (value); + + return alignment; +} + +static void +set_ordered_list_type_to_element (WebKitDOMElement *list, + EHTMLEditorSelectionBlockFormat format) +{ + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST) + webkit_dom_element_remove_attribute (list, "type"); + else if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA) + webkit_dom_element_set_attribute (list, "type", "A", NULL); + else if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN) + webkit_dom_element_set_attribute (list, "type", "I", NULL); +} + +static WebKitDOMElement * +create_list_element (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + EHTMLEditorSelectionBlockFormat format, + gint level, + gboolean html_mode) +{ + WebKitDOMElement *list; + gint offset = -SPACES_PER_LIST_LEVEL; + gboolean inserting_unordered_list = + format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST; + + list = webkit_dom_document_create_element ( + document, inserting_unordered_list ? "UL" : "OL", NULL); + + set_ordered_list_type_to_element (list, format); + + if (level >= 0) + offset = (level + 1) * -SPACES_PER_LIST_LEVEL; + + if (!html_mode) + e_html_editor_selection_set_paragraph_style ( + selection, list, -1, offset, ""); + + return list; +} + +static void +remove_node (WebKitDOMNode *node) +{ + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), node, NULL); +} + +static void +format_change_list_from_list (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + EHTMLEditorSelectionBlockFormat to, + gboolean html_mode) +{ + gboolean after_selection_end = FALSE; + WebKitDOMElement *selection_start_marker, *selection_end_marker, *new_list; + WebKitDOMNode *source_list, *source_list_clone, *current_list, *item; + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + + if (!selection_start_marker || !selection_end_marker) + return; + + new_list = create_list_element (selection, document, to, 0, html_mode); + + /* Copy elements from previous block to list */ + item = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + source_list = webkit_dom_node_get_parent_node (item); + current_list = source_list; + source_list_clone = webkit_dom_node_clone_node (source_list, FALSE); + + if (element_has_class (WEBKIT_DOM_ELEMENT (source_list), "-x-evo-indented")) + element_add_class (WEBKIT_DOM_ELEMENT (new_list), "-x-evo-indented"); + + while (item) { + WebKitDOMNode *next_item = webkit_dom_node_get_next_sibling (item); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + webkit_dom_node_append_child ( + after_selection_end ? + source_list_clone : WEBKIT_DOM_NODE (new_list), + WEBKIT_DOM_NODE (item), + NULL); + } + + if (webkit_dom_node_contains (item, WEBKIT_DOM_NODE (selection_end_marker))) { + source_list_clone = webkit_dom_node_clone_node (current_list, FALSE); + after_selection_end = TRUE; + } + + if (!next_item) { + if (after_selection_end) + break; + current_list = webkit_dom_node_get_next_sibling (current_list); + next_item = webkit_dom_node_get_first_child (current_list); + } + item = next_item; + } + + if (webkit_dom_node_has_child_nodes (source_list_clone)) + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + WEBKIT_DOM_NODE (source_list_clone), + webkit_dom_node_get_next_sibling (source_list), NULL); + if (webkit_dom_node_has_child_nodes (WEBKIT_DOM_NODE (new_list))) + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + WEBKIT_DOM_NODE (new_list), + webkit_dom_node_get_next_sibling (source_list), NULL); + if (!webkit_dom_node_has_child_nodes (source_list)) + remove_node (source_list); +} + +/** + * e_html_editor_selection_set_alignment: + * @selection: an #EHTMLEditorSelection + * @alignment: an #EHTMLEditorSelectionAlignment value to apply + * + * Sets alignment of current paragraph to give @alignment. + */ +void +e_html_editor_selection_set_alignment (EHTMLEditorSelection *selection, + EHTMLEditorSelectionAlignment alignment) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + const gchar *class = ""; + WebKitDOMDocument *document; + WebKitDOMElement *selection_start_marker; + WebKitDOMNode *parent; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_get_alignment (selection) == alignment) + return; + + switch (alignment) { + case E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER: + command = E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_CENTER; + class = "-x-evo-list-item-align-center"; + break; + + case E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT: + command = E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_LEFT; + break; + + case E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT: + command = E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_RIGHT; + class = "-x-evo-list-item-align-right"; + break; + } + + selection->priv->alignment = alignment; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + e_html_editor_selection_save (selection); + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + + if (!selection_start_marker) { + g_object_unref (view); + return; + } + + parent = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (parent)) { + element_remove_class ( + WEBKIT_DOM_ELEMENT (parent), + "-x-evo-list-item-align-center"); + element_remove_class ( + WEBKIT_DOM_ELEMENT (parent), + "-x-evo-list-item-align-right"); + + element_add_class (WEBKIT_DOM_ELEMENT (parent), class); + } else { + e_html_editor_view_exec_command (view, command, NULL); + } + + e_html_editor_selection_restore (selection); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "alignment"); +} + +/** + * e_html_editor_selection_get_background_color: + * @selection: an #EHTMLEditorSelection + * + * Returns background color of currently selected text or letter at current + * cursor position. + * + * Returns: A string with code of current background color. + */ +const gchar * +e_html_editor_selection_get_background_color (EHTMLEditorSelection *selection) +{ + WebKitDOMNode *ancestor; + WebKitDOMRange *range; + WebKitDOMCSSStyleDeclaration *css; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + range = html_editor_selection_get_current_range (selection); + + ancestor = webkit_dom_range_get_common_ancestor_container (range, NULL); + + css = webkit_dom_element_get_style (WEBKIT_DOM_ELEMENT (ancestor)); + selection->priv->background_color = + webkit_dom_css_style_declaration_get_property_value ( + css, "background-color"); + + return selection->priv->background_color; +} + +/** + * e_html_editor_selection_set_background_color: + * @selection: an #EHTMLEditorSelection + * @color: code of new background color to set + * + * Changes background color of current selection or letter at current cursor + * position to @color. + */ +void +e_html_editor_selection_set_background_color (EHTMLEditorSelection *selection, + const gchar *color) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (color != NULL && *color != '\0'); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_BACKGROUND_COLOR; + e_html_editor_view_exec_command (view, command, color); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "background-color"); +} + +static gint +get_indentation_level (WebKitDOMElement *element) +{ + WebKitDOMElement *parent; + gint level = 0; + + if (element_has_class (element, "-x-evo-indented")) + level++; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (element)); + /* Count level of indentation */ + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (element_has_class (parent, "-x-evo-indented")) + level++; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (parent)); + } + + return level; +} + +static WebKitDOMNode * +get_block_node (WebKitDOMRange *range) +{ + WebKitDOMNode *node; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + if (!WEBKIT_DOM_IS_ELEMENT (node)) + node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node)); + + if (element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-temp-text-wrapper")) + node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node)); + + return node; +} + +/** + * e_html_editor_selection_get_list_format_from_node: + * @node: an #WebKitDOMNode + * + * Returns block format of given list. + * + * Returns: #EHTMLEditorSelectionBlockFormat + */ +EHTMLEditorSelectionBlockFormat +e_html_editor_selection_get_list_format_from_node (WebKitDOMNode *node) +{ + EHTMLEditorSelectionBlockFormat format = + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST; + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (node)) + return -1; + + if (WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (node)) + return format; + + if (WEBKIT_DOM_IS_ELEMENT (node)) { + gchar *type_value = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "type"); + + if (!type_value) + return E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST; + + if (!*type_value) + format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST; + else if (g_ascii_strcasecmp (type_value, "A") == 0) + format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA; + else if (g_ascii_strcasecmp (type_value, "I") == 0) + format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN; + g_free (type_value); + } + + return -1; +} + +/** + * e_html_editor_selection_get_block_format: + * @selection: an #EHTMLEditorSelection + * + * Returns block format of current paragraph. + * + * Returns: #EHTMLEditorSelectionBlockFormat + */ +EHTMLEditorSelectionBlockFormat +e_html_editor_selection_get_block_format (EHTMLEditorSelection *selection) +{ + WebKitDOMNode *node; + WebKitDOMRange *range; + WebKitDOMElement *element; + EHTMLEditorSelectionBlockFormat result; + + g_return_val_if_fail ( + E_IS_HTML_EDITOR_SELECTION (selection), + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH; + + node = webkit_dom_range_get_start_container (range, NULL); + + if (e_html_editor_dom_node_find_parent_element (node, "UL")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST; + } else if ((element = e_html_editor_dom_node_find_parent_element (node, "OL")) != NULL) { + if (webkit_dom_element_has_attribute (element, "type")) { + gchar *type; + + type = webkit_dom_element_get_attribute (element, "type"); + if (type && ((*type == 'a') || (*type == 'A'))) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA; + } else if (type && ((*type == 'i') || (*type == 'I'))) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN; + } else { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST; + } + + g_free (type); + } else { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST; + } + } else if (e_html_editor_dom_node_find_parent_element (node, "PRE")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PRE; + } else if (e_html_editor_dom_node_find_parent_element (node, "ADDRESS")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ADDRESS; + } else if (e_html_editor_dom_node_find_parent_element (node, "H1")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H1; + } else if (e_html_editor_dom_node_find_parent_element (node, "H2")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H2; + } else if (e_html_editor_dom_node_find_parent_element (node, "H3")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H3; + } else if (e_html_editor_dom_node_find_parent_element (node, "H4")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H4; + } else if (e_html_editor_dom_node_find_parent_element (node, "H5")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H5; + } else if (e_html_editor_dom_node_find_parent_element (node, "H6")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H6; + } else if ((element = e_html_editor_dom_node_find_parent_element (node, "BLOCKQUOTE")) != NULL) { + if (element_has_class (element, "-x-evo-indented")) + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH; + else { + WebKitDOMNode *block = get_block_node (range); + + if (element_has_class (WEBKIT_DOM_ELEMENT (block), "-x-evo-paragraph")) + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH; + else + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_BLOCKQUOTE; + } + } else if (e_html_editor_dom_node_find_parent_element (node, "P")) { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH; + } else { + result = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH; + } + + return result; +} + +static gboolean +is_caret_position_node (WebKitDOMNode *node) +{ + return element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-caret-position"); +} + +static void +merge_list_into_list (WebKitDOMNode *from, + WebKitDOMNode *to, + gboolean insert_before) +{ + WebKitDOMNode *item; + + if (!(to && from)) + return; + + while ((item = webkit_dom_node_get_first_child (from)) != NULL) { + if (insert_before) + webkit_dom_node_insert_before ( + to, item, webkit_dom_node_get_last_child (to), NULL); + else + webkit_dom_node_append_child (to, item, NULL); + } + + if (!webkit_dom_node_has_child_nodes (from)) + remove_node (from); + +} + +static void +merge_lists_if_possible (WebKitDOMNode *list) +{ + EHTMLEditorSelectionBlockFormat format, prev, next; + WebKitDOMNode *prev_sibling, *next_sibling; + + prev_sibling = webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (list)); + next_sibling = webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (list)); + + format = e_html_editor_selection_get_list_format_from_node (list), + prev = e_html_editor_selection_get_list_format_from_node (prev_sibling); + next = e_html_editor_selection_get_list_format_from_node (next_sibling); + + if (format == prev && format != -1 && prev != -1) + merge_list_into_list (prev_sibling, list, TRUE); + + if (format == next && format != -1 && next != -1) + merge_list_into_list (next_sibling, list, FALSE); +} + +static void +remove_wrapping (WebKitDOMElement *element) +{ + WebKitDOMNodeList *list; + gint ii, length; + + list = webkit_dom_element_query_selector_all ( + element, "br.-x-evo-wrap-br", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) + remove_node (webkit_dom_node_list_item (list, ii)); +} + +static void +remove_quoting (WebKitDOMElement *element) +{ + WebKitDOMNodeList *list; + gint ii, length; + + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-quoted", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + remove_node (webkit_dom_node_list_item (list, ii)); + } + + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-temp-text-wrapper", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *nd = webkit_dom_node_list_item (list, ii); + + while (webkit_dom_node_has_child_nodes (nd)) { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (nd), + webkit_dom_node_get_first_child (nd), + nd, + NULL); + } + + remove_node (nd); + } + + webkit_dom_node_normalize (WEBKIT_DOM_NODE (element)); +} + +static gboolean +node_is_list (WebKitDOMNode *node) +{ + return node && ( + WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (node) || + WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (node)); +} + +static gint +get_citation_level (WebKitDOMNode *node) +{ + WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node); + gint level = 0; + + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (parent) && + webkit_dom_element_has_attribute (WEBKIT_DOM_ELEMENT (parent), "type")) + level++; + + parent = webkit_dom_node_get_parent_node (parent); + } + + return level; +} + +static void +format_change_block_to_block (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format, + EHTMLEditorView *view, + const gchar *value, + WebKitDOMDocument *document) +{ + gboolean after_selection_end, quoted = FALSE; + WebKitDOMElement *selection_start_marker, *selection_end_marker, *element; + WebKitDOMNode *block, *next_block; + + e_html_editor_selection_save (selection); + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + block = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + if (element_has_class (WEBKIT_DOM_ELEMENT (block), "-x-evo-temp-text-wrapper")) { + block = webkit_dom_node_get_parent_node (block); + remove_wrapping (WEBKIT_DOM_ELEMENT (block)); + remove_quoting (WEBKIT_DOM_ELEMENT (block)); + quoted = TRUE; + } + + /* Process all blocks that are in the selection one by one */ + while (block) { + WebKitDOMNode *child; + + after_selection_end = webkit_dom_node_contains ( + block, WEBKIT_DOM_NODE (selection_end_marker)); + + next_block = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (block)); + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH) + element = e_html_editor_selection_get_paragraph_element ( + selection, document, -1, 0); + else + element = webkit_dom_document_create_element ( + document, value, NULL); + + while ((child = webkit_dom_node_get_first_child (block))) + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), child, NULL); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (block), + WEBKIT_DOM_NODE (element), + block, + NULL); + + remove_node (block); + + block = next_block; + + if (after_selection_end) + break; + } + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH && + !e_html_editor_view_get_html_mode (view)) { + gint citation_level, quote; + + citation_level = get_citation_level (WEBKIT_DOM_NODE (element)); + quote = citation_level ? citation_level + 1 : 0; + + element = e_html_editor_selection_wrap_paragraph_length ( + selection, element, selection->priv->word_wrap_length - quote); + } + + if (quoted) + e_html_editor_view_quote_plain_text_element (view, element); + + e_html_editor_selection_restore (selection); +} + +static void +remove_node_if_empty (WebKitDOMNode *node) +{ + if (!WEBKIT_DOM_IS_NODE (node)) + return; + + if (!webkit_dom_node_get_first_child (node)) { + remove_node (node); + } else { + gchar *text_content; + + text_content = webkit_dom_node_get_text_content (node); + if (!text_content) + remove_node (node); + + if (text_content && !*text_content) + remove_node (node); + + g_free (text_content); + } +} + +static void +format_change_block_to_list (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format, + EHTMLEditorView *view, + WebKitDOMDocument *document) +{ + gboolean after_selection_end; + gboolean html_mode = e_html_editor_view_get_html_mode (view); + WebKitDOMElement *selection_start_marker, *selection_end_marker, *item, *list; + WebKitDOMNode *block, *next_block; + + e_html_editor_selection_save (selection); + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + block = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + + list = create_list_element (selection, document, format, 0, html_mode); + + if (element_has_class (WEBKIT_DOM_ELEMENT (block), "-x-evo-temp-text-wrapper")) { + WebKitDOMElement *element; + + block = webkit_dom_node_get_parent_node (block); + + remove_wrapping (WEBKIT_DOM_ELEMENT (block)); + remove_quoting (WEBKIT_DOM_ELEMENT (block)); + + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, NULL); + + element = webkit_dom_document_query_selector ( + document, "body>br", NULL); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + WEBKIT_DOM_NODE (list), + WEBKIT_DOM_NODE (element), + NULL); + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + block = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + } else + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (block), + WEBKIT_DOM_NODE (list), + block, + NULL); + + /* Process all blocks that are in the selection one by one */ + while (block) { + gboolean empty = FALSE; + gchar *content; + WebKitDOMNode *child; + + after_selection_end = webkit_dom_node_contains ( + block, WEBKIT_DOM_NODE (selection_end_marker)); + + next_block = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (block)); + + item = webkit_dom_document_create_element (document, "LI", NULL); + content = webkit_dom_node_get_text_content (block); + + empty = !*content || (g_strcmp0 (content, UNICODE_ZERO_WIDTH_SPACE) == 0); + g_free (content); + + /* We have to use again the hidden space to move caret into newly inserted list */ + if (empty) { + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (item), + UNICODE_ZERO_WIDTH_SPACE, + NULL); + } + + while ((child = webkit_dom_node_get_first_child (block))) + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (item), child, NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (list), WEBKIT_DOM_NODE (item), NULL); + + remove_node (block); + + block = next_block; + + if (after_selection_end) + break; + } + + merge_lists_if_possible (WEBKIT_DOM_NODE (list)); + + e_html_editor_view_force_spell_check (view); + e_html_editor_selection_restore (selection); +} + +static void +format_change_list_to_list (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format, + WebKitDOMDocument *document, + gboolean html_mode) +{ + EHTMLEditorSelectionBlockFormat prev = 0, next = 0; + gboolean done = FALSE, indented = FALSE; + gboolean selection_starts_in_first_child, selection_ends_in_last_child; + WebKitDOMElement *selection_start_marker, *selection_end_marker; + WebKitDOMNode *prev_list, *current_list, *next_list; + + e_html_editor_selection_save (selection); + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + + current_list = webkit_dom_node_get_parent_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker))); + + prev_list = webkit_dom_node_get_parent_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker))); + + next_list = webkit_dom_node_get_parent_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_end_marker))); + + selection_starts_in_first_child = + webkit_dom_node_contains ( + webkit_dom_node_get_first_child (current_list), + WEBKIT_DOM_NODE (selection_start_marker)); + + selection_ends_in_last_child = + webkit_dom_node_contains ( + webkit_dom_node_get_last_child (current_list), + WEBKIT_DOM_NODE (selection_end_marker)); + + indented = element_has_class (WEBKIT_DOM_ELEMENT (current_list), "-x-evo-indented"); + + if (!prev_list || !next_list || indented) { + format_change_list_from_list (selection, document, format, html_mode); + goto out; + } + + if (webkit_dom_node_is_same_node (prev_list, next_list)) { + prev_list = webkit_dom_node_get_previous_sibling ( + webkit_dom_node_get_parent_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)))); + next_list = webkit_dom_node_get_next_sibling ( + webkit_dom_node_get_parent_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_end_marker)))); + if (!prev_list || !next_list) { + format_change_list_from_list (selection, document, format, html_mode); + goto out; + } + } + + prev = e_html_editor_selection_get_list_format_from_node (prev_list); + next = e_html_editor_selection_get_list_format_from_node (next_list); + + if (format == prev && format != -1 && prev != -1) { + if (selection_starts_in_first_child && selection_ends_in_last_child) { + done = TRUE; + merge_list_into_list (current_list, prev_list, FALSE); + } + } + + if (format == next && format != -1 && next != -1) { + if (selection_starts_in_first_child && selection_ends_in_last_child) { + done = TRUE; + merge_list_into_list (next_list, prev_list, FALSE); + } + } + + if (done) + goto out; + + format_change_list_from_list (selection, document, format, html_mode); +out: + e_html_editor_selection_restore (selection); +} + +static void +format_change_list_to_block (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format, + WebKitDOMDocument *document) +{ + gboolean after_end = FALSE; + WebKitDOMElement *selection_start, *element, *selection_end; + WebKitDOMNode *source_list, *next_item, *item, *source_list_clone; + + e_html_editor_selection_save (selection); + + selection_start = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + + item = webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (selection_start)); + source_list = webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (item)); + source_list_clone = webkit_dom_node_clone_node (source_list, FALSE); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + WEBKIT_DOM_NODE (source_list_clone), + webkit_dom_node_get_next_sibling (source_list), + NULL); + + next_item = item; + + /* Process all nodes that are in selection one by one */ + while (next_item) { + WebKitDOMNode *tmp; + + tmp = webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (next_item)); + + if (!after_end) { + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH) + element = e_html_editor_selection_get_paragraph_element (selection, document, -1, 0); + else if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PRE) + element = webkit_dom_document_create_element (document, "PRE", NULL); + else if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_BLOCKQUOTE) + element = webkit_dom_document_create_element (document, "BLOCKQUOTE", NULL); + else + element = e_html_editor_selection_get_paragraph_element (selection, document, -1, 0); + + after_end = webkit_dom_node_contains (next_item, WEBKIT_DOM_NODE (selection_end)); + + while (webkit_dom_node_get_first_child (next_item)) { + WebKitDOMNode *node = webkit_dom_node_get_first_child (next_item); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), node, NULL); + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + WEBKIT_DOM_NODE (element), + source_list_clone, + NULL); + + remove_node (next_item); + + next_item = tmp; + } else { + webkit_dom_node_append_child ( + source_list_clone, next_item, NULL); + + next_item = tmp; + } + } + + remove_node_if_empty (source_list_clone); + remove_node_if_empty (source_list); + + e_html_editor_selection_restore (selection); +} + +/** + * e_html_editor_selection_set_block_format: + * @selection: an #EHTMLEditorSelection + * @format: an #EHTMLEditorSelectionBlockFormat value + * + * Changes block format of current paragraph to @format. + */ +void +e_html_editor_selection_set_block_format (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format) +{ + EHTMLEditorView *view; + EHTMLEditorSelectionBlockFormat current_format; + const gchar *value; + gboolean has_selection = FALSE; + gboolean from_list = FALSE, to_list = FALSE, html_mode; + WebKitDOMDocument *document; + WebKitDOMRange *range; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + current_format = e_html_editor_selection_get_block_format (selection); + if (current_format == format) { + return; + } + + switch (format) { + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_BLOCKQUOTE: + value = "BLOCKQUOTE"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H1: + value = "H1"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H2: + value = "H2"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H3: + value = "H3"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H4: + value = "H4"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H5: + value = "H5"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H6: + value = "H6"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH: + value = "P"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PRE: + value = "PRE"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ADDRESS: + value = "ADDRESS"; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST: + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA: + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN: + to_list = TRUE; + value = NULL; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST: + to_list = TRUE; + value = NULL; + break; + case E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE: + default: + value = NULL; + break; + } + + if (g_strcmp0 (e_html_editor_selection_get_string (selection), "") != 0) + has_selection = TRUE; + + /* H1 - H6 have bold font by default */ + if (format >= E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H1 && + format <= E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H6) + selection->priv->is_bold = TRUE; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + html_mode = e_html_editor_view_get_html_mode (view); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + from_list = + current_format >= E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST; + + range = html_editor_selection_get_current_range (selection); + if (!range) { + g_object_unref (view); + return; + } + + if (from_list && to_list) + format_change_list_to_list (selection, format, document, html_mode); + + if (!from_list && !to_list) + format_change_block_to_block (selection, format, view, value, document); + + if (from_list && !to_list) + format_change_list_to_block (selection, format, document); + + if (!from_list && to_list) + format_change_block_to_list (selection, format, view, document); + + if (!has_selection) + e_html_editor_view_force_spell_check (view); + + g_object_unref (view); + + /* When changing the format we need to re-set the alignment */ + e_html_editor_selection_set_alignment (selection, selection->priv->alignment); + + g_object_notify (G_OBJECT (selection), "block-format"); +} + +/** + * e_html_editor_selection_get_font_color: + * @selection: an #EHTMLEditorSelection + * @rgba: a #GdkRGBA object to be set to current font color + * + * Sets @rgba to contain color of current text selection or letter at current + * cursor position. + */ +void +e_html_editor_selection_get_font_color (EHTMLEditorSelection *selection, + GdkRGBA *rgba) +{ + gchar *color; + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (g_strcmp0 (e_html_editor_selection_get_string (selection), "") == 0) { + color = g_strdup (selection->priv->font_color); + } else { + color = get_font_property (selection, "color"); + if (!color) { + *rgba = black; + return; + } + } + + gdk_rgba_parse (rgba, color); + g_free (color); +} + +/** + * e_html_editor_selection_set_font_color: + * @selection: an #EHTMLEditorSelection + * @rgba: a #GdkRGBA + * + * Sets font color of current selection or letter at current cursor position to + * color defined in @rgba. + */ +void +e_html_editor_selection_set_font_color (EHTMLEditorSelection *selection, + const GdkRGBA *rgba) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + guint32 rgba_value; + gchar *color; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (!rgba) + rgba = &black; + + rgba_value = e_rgba_to_value ((GdkRGBA *) rgba); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_FORE_COLOR; + color = g_strdup_printf ("#%06x", rgba_value); + selection->priv->font_color = g_strdup (color); + e_html_editor_view_exec_command (view, command, color); + g_free (color); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "font-color"); +} + +/** + * e_html_editor_selection_get_font_name: + * @selection: an #EHTMLEditorSelection + * + * Returns name of font used in current selection or at letter at current cursor + * position. + * + * Returns: A string with font name. [transfer-none] + */ +const gchar * +e_html_editor_selection_get_font_name (EHTMLEditorSelection *selection) +{ + WebKitDOMNode *node; + WebKitDOMRange *range; + WebKitDOMCSSStyleDeclaration *css; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + range = html_editor_selection_get_current_range (selection); + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + + g_free (selection->priv->font_family); + css = webkit_dom_element_get_style (WEBKIT_DOM_ELEMENT (node)); + selection->priv->font_family = + webkit_dom_css_style_declaration_get_property_value (css, "fontFamily"); + + return selection->priv->font_family; +} + +/** + * e_html_editor_selection_set_font_name: + * @selection: an #EHTMLEditorSelection + * @font_name: a font name to apply + * + * Sets font name of current selection or of letter at current cursor position + * to @font_name. + */ +void +e_html_editor_selection_set_font_name (EHTMLEditorSelection *selection, + const gchar *font_name) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_FONT_NAME; + e_html_editor_view_exec_command (view, command, font_name); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "font-name"); +} + +/** + * e_editor_Selection_get_font_size: + * @selection: an #EHTMLEditorSelection + * + * Returns point size of current selection or of letter at current cursor position. + */ + guint +e_html_editor_selection_get_font_size (EHTMLEditorSelection *selection) +{ + gchar *size; + guint size_int; + + g_return_val_if_fail ( + E_IS_HTML_EDITOR_SELECTION (selection), + E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL); + + size = get_font_property (selection, "size"); + if (!size) + return E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL; + + size_int = atoi (size); + g_free (size); + + if (size_int == 0) + return E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL; + + return size_int; +} + +/** + * e_html_editor_selection_set_font_size: + * @selection: an #EHTMLEditorSelection + * @font_size: point size to apply + * + * Sets font size of current selection or of letter at current cursor position + * to @font_size. + */ +void +e_html_editor_selection_set_font_size (EHTMLEditorSelection *selection, + guint font_size) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + gchar *size_str; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + selection->priv->font_size = font_size; + command = E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE; + size_str = g_strdup_printf ("%d", font_size); + e_html_editor_view_exec_command (view, command, size_str); + g_free (size_str); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "font-size"); +} + +/** + * e_html_editor_selection_is_citation: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current paragraph is a citation. + * + * Returns: @TRUE when current paragraph is a citation, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_citation (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + WebKitDOMNode *node; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + + if (WEBKIT_DOM_IS_TEXT (node)) + return get_has_style (selection, "citation"); + + /* If we are changing the format of block we have to re-set bold property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return FALSE; + } + g_free (text_content); + + value = webkit_dom_element_get_attribute (WEBKIT_DOM_ELEMENT (node), "type"); + + /* citation == <blockquote type='cite'> */ + if (g_strstr_len (value, -1, "cite")) + ret_val = TRUE; + else + ret_val = get_has_style (selection, "citation"); + + g_free (value); + return ret_val; +} + +/** + * e_html_editor_selection_is_indented: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current paragraph is indented. This does not include + * citations. To check, whether paragraph is a citation, use + * e_html_editor_selection_is_citation(). + * + * Returns: @TRUE when current paragraph is indented, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_indented (EHTMLEditorSelection *selection) +{ + WebKitDOMRange *range; + WebKitDOMNode *node; + WebKitDOMElement *element; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_end_container (range, NULL); + if (!WEBKIT_DOM_IS_ELEMENT (node)) + node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node)); + + element = webkit_dom_node_get_parent_element (node); + + return element_has_class (element, "-x-evo-indented"); +} + +static gboolean +is_in_html_mode (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view = e_html_editor_selection_ref_html_editor_view (selection); + gboolean ret_val; + + g_return_val_if_fail (view != NULL, FALSE); + + ret_val = e_html_editor_view_get_html_mode (view); + + g_object_unref (view); + + return ret_val; +} + +static gint +get_list_level (WebKitDOMNode *node) +{ + gint level = 0; + + while (node && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (node)) { + if (node_is_list (node)) + level++; + node = webkit_dom_node_get_parent_node (node); + } + + return level; +} + +/** + * e_html_editor_selection_indent: + * @selection: an #EHTMLEditorSelection + * + * Indents current paragraph by one level. + */ +void +e_html_editor_selection_indent (EHTMLEditorSelection *selection) +{ + gboolean has_selection; + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + has_selection = g_strcmp0 (e_html_editor_selection_get_string (selection), "") != 0; + + if (!has_selection) { + WebKitDOMDocument *document; + WebKitDOMRange *range; + WebKitDOMNode *node, *clone; + WebKitDOMElement *element, *caret_position; + gint word_wrap_length = selection->priv->word_wrap_length; + gint level; + gint final_width = 0; + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + caret_position = e_html_editor_selection_save_caret_position (selection); + + range = html_editor_selection_get_current_range (selection); + if (!range) { + g_object_unref (view); + return; + } + + node = webkit_dom_range_get_end_container (range, NULL); + if (!WEBKIT_DOM_IS_ELEMENT (node)) + node = WEBKIT_DOM_NODE ( + webkit_dom_node_get_parent_element (node)); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (node)) { + gboolean html_mode = e_html_editor_view_get_html_mode (view); + WebKitDOMElement *list; + WebKitDOMNode *source_list = webkit_dom_node_get_parent_node (node); + EHTMLEditorSelectionBlockFormat format; + + format = e_html_editor_selection_get_list_format_from_node (source_list); + + list = create_list_element ( + selection, document, format, get_list_level (node), html_mode); + + element_add_class (list, "-x-evo-indented"); + webkit_dom_node_insert_before ( + source_list, WEBKIT_DOM_NODE (list), node, NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (list), node, NULL); + if (!webkit_dom_node_contains (node, WEBKIT_DOM_NODE (caret_position))) { + webkit_dom_node_append_child ( + node, WEBKIT_DOM_NODE (caret_position), NULL); + } + + merge_lists_if_possible (WEBKIT_DOM_NODE (list)); + + e_html_editor_selection_restore_caret_position (selection); + + g_object_unref (view); + return; + } + + level = get_indentation_level (WEBKIT_DOM_ELEMENT (node)); + + final_width = word_wrap_length - SPACES_PER_INDENTATION * (level + 1); + if (final_width < 10 && !is_in_html_mode (selection)) { + e_html_editor_selection_restore_caret_position (selection); + g_object_unref (view); + return; + } + + element = webkit_dom_node_get_parent_element (node); + clone = webkit_dom_node_clone_node (node, TRUE); + + /* Remove style and let the paragraph inherit it from parent */ + if (element_has_class (WEBKIT_DOM_ELEMENT (clone), "-x-evo-paragraph")) + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (clone), "style"); + + element = e_html_editor_selection_get_indented_element ( + selection, document, final_width); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + clone, + NULL); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + node, + NULL); + + e_html_editor_selection_restore_caret_position (selection); + } else { + command = E_HTML_EDITOR_VIEW_COMMAND_INDENT; + e_html_editor_selection_save (selection); + e_html_editor_view_exec_command (view, command, NULL); + } + + e_html_editor_view_force_spell_check_for_current_paragraph (view); + + if (has_selection) + e_html_editor_selection_restore (selection); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "indented"); +} + +static const gchar * +get_css_alignment_value (EHTMLEditorSelectionAlignment alignment) +{ + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) + return ""; /* Left is by default on ltr */ + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) + return "text-align: center;"; + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) + return "text-align: right;"; + + return ""; +} + +static void +unindent_list (EHTMLEditorSelection *selection, + WebKitDOMDocument *document) +{ + gboolean after = FALSE; + WebKitDOMElement *new_list; + WebKitDOMNode *source_list, *source_list_clone, *current_list, *item; + WebKitDOMElement *selection_start_marker; + WebKitDOMElement *selection_end_marker; + + selection_start_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-start-marker", NULL); + selection_end_marker = webkit_dom_document_query_selector ( + document, "span#-x-evo-selection-end-marker", NULL); + + if (!selection_start_marker || !selection_end_marker) + return; + + /* Copy elements from previous block to list */ + item = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (selection_start_marker)); + source_list = webkit_dom_node_get_parent_node (item); + new_list = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_clone_node (source_list, FALSE)); + current_list = source_list; + source_list_clone = webkit_dom_node_clone_node (source_list, FALSE); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + WEBKIT_DOM_NODE (source_list_clone), + webkit_dom_node_get_next_sibling (source_list), + NULL); + + if (element_has_class (WEBKIT_DOM_ELEMENT (source_list), "-x-evo-indented")) + element_add_class (WEBKIT_DOM_ELEMENT (new_list), "-x-evo-indented"); + + while (item) { + WebKitDOMNode *next_item = webkit_dom_node_get_next_sibling (item); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + if (after) { + webkit_dom_node_append_child ( + source_list_clone, WEBKIT_DOM_NODE (item), NULL); + } else { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (source_list), + item, + webkit_dom_node_get_next_sibling (source_list), + NULL); + } + } + + if (webkit_dom_node_contains (item, WEBKIT_DOM_NODE (selection_end_marker))) + after = TRUE; + + if (!next_item) { + if (after) + break; + + current_list = webkit_dom_node_get_next_sibling (current_list); + next_item = webkit_dom_node_get_first_child (current_list); + } + item = next_item; + } + + remove_node_if_empty (source_list_clone); + remove_node_if_empty (source_list); +} +/** + * e_html_editor_selection_unindent: + * @selection: an #EHTMLEditorSelection + * + * Unindents current paragraph by one level. + */ +void +e_html_editor_selection_unindent (EHTMLEditorSelection *selection) +{ + gboolean has_selection; + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + has_selection = g_strcmp0 (e_html_editor_selection_get_string (selection), "") != 0; + + if (!has_selection) { + EHTMLEditorSelectionAlignment alignment; + gboolean before_node = TRUE, reinsert_caret_position = FALSE; + const gchar *align_value; + gint word_wrap_length = selection->priv->word_wrap_length; + gint level, width; + WebKitDOMDocument *document; + WebKitDOMElement *element; + WebKitDOMElement *prev_blockquote = NULL, *next_blockquote = NULL; + WebKitDOMNode *node, *clone, *node_clone, *caret_node; + WebKitDOMRange *range; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + e_html_editor_selection_save_caret_position (selection); + + alignment = e_html_editor_selection_get_alignment (selection); + align_value = get_css_alignment_value (alignment); + + range = html_editor_selection_get_current_range (selection); + if (!range) { + g_object_unref (view); + return; + } + + node = webkit_dom_range_get_end_container (range, NULL); + if (!WEBKIT_DOM_IS_ELEMENT (node)) + node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node)); + + element = webkit_dom_node_get_parent_element (node); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (node)) { + e_html_editor_selection_save (selection); + e_html_editor_selection_clear_caret_position_marker (selection); + + unindent_list (selection, document); + e_html_editor_selection_restore (selection); + g_object_unref (view); + return; + } + + if (!WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (element)) + return; + + element_add_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-to-unindent"); + + level = get_indentation_level (element); + width = word_wrap_length - SPACES_PER_INDENTATION * level; + clone = WEBKIT_DOM_NODE (webkit_dom_node_clone_node (WEBKIT_DOM_NODE (element), TRUE)); + + /* Look if we have previous siblings, if so, we have to + * create new blockquote that will include them */ + if (webkit_dom_node_get_previous_sibling (node)) + prev_blockquote = e_html_editor_selection_get_indented_element ( + selection, document, width); + + /* Look if we have next siblings, if so, we have to + * create new blockquote that will include them */ + if (webkit_dom_node_get_next_sibling (node)) + next_blockquote = e_html_editor_selection_get_indented_element ( + selection, document, width); + + /* Copy nodes that are before / after the element that we want to unindent */ + while (webkit_dom_node_has_child_nodes (clone)) { + WebKitDOMNode *child; + + child = webkit_dom_node_get_first_child (clone); + + if (is_caret_position_node (child)) { + reinsert_caret_position = TRUE; + caret_node = webkit_dom_node_clone_node (child, TRUE); + remove_node (child); + continue; + } + + if (webkit_dom_node_is_equal_node (child, node)) { + before_node = FALSE; + node_clone = webkit_dom_node_clone_node (child, TRUE); + remove_node (child); + continue; + } + + webkit_dom_node_append_child ( + before_node ? + WEBKIT_DOM_NODE (prev_blockquote) : + WEBKIT_DOM_NODE (next_blockquote), + child, + NULL); + + remove_node (child); + } + + element_remove_class (WEBKIT_DOM_ELEMENT (node_clone), "-x-evo-to-unindent"); + + /* Insert blockqoute with nodes that were before the element that we want to unindent */ + if (prev_blockquote) { + if (webkit_dom_node_has_child_nodes (WEBKIT_DOM_NODE (prev_blockquote))) { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + WEBKIT_DOM_NODE (prev_blockquote), + WEBKIT_DOM_NODE (element), + NULL); + } + } + + /* Reinsert the caret position */ + if (reinsert_caret_position) { + webkit_dom_node_insert_before ( + node_clone, + caret_node, + webkit_dom_node_get_first_child (node_clone), + NULL); + } + + if (level == 1 && element_has_class (WEBKIT_DOM_ELEMENT (node_clone), "-x-evo-paragraph")) + e_html_editor_selection_set_paragraph_style ( + selection, WEBKIT_DOM_ELEMENT (node_clone), word_wrap_length, 0, align_value); + + /* Insert the unindented element */ + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + node_clone, + WEBKIT_DOM_NODE (element), + NULL); + + /* Insert blockqoute with nodes that were after the element that we want to unindent */ + if (next_blockquote) { + if (webkit_dom_node_has_child_nodes (WEBKIT_DOM_NODE (prev_blockquote))) { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + WEBKIT_DOM_NODE (next_blockquote), + WEBKIT_DOM_NODE (element), + NULL); + } + } + + /* Remove old blockquote */ + remove_node (WEBKIT_DOM_NODE (element)); + + e_html_editor_selection_restore_caret_position (selection); + } else { + command = E_HTML_EDITOR_VIEW_COMMAND_OUTDENT; + e_html_editor_selection_save (selection); + e_html_editor_view_exec_command (view, command, NULL); + } + + e_html_editor_view_force_spell_check_for_current_paragraph (view); + + if (has_selection) + e_html_editor_selection_restore (selection); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "indented"); +} + +/** + * e_html_editor_selection_is_bold: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is bold. + * + * Returns @TRUE when selection is bold, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_bold (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + EHTMLEditorView *view; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + /* If we are changing the format of block we have to re-set bold property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return selection->priv->is_bold; + } + g_free (text_content); + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "font-weight"); + + if (g_strstr_len (value, -1, "normal")) + ret_val = FALSE; + else + ret_val = TRUE; + + g_free (value); + return ret_val; +} + +/** + * e_html_editor_selection_set_bold: + * @selection: an #EHTMLEditorSelection + * @bold: @TRUE to enable bold, @FALSE to disable + * + * Toggles bold formatting of current selection or letter at current cursor + * position, depending on whether @bold is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_bold (EHTMLEditorSelection *selection, + gboolean bold) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_bold (selection) == bold) + return; + + selection->priv->is_bold = bold; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_BOLD; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "bold"); +} + +/** + * e_html_editor_selection_is_italic: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is italic. + * + * Returns @TRUE when selection is italic, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_italic (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + EHTMLEditorView *view; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + /* If we are changing the format of block we have to re-set italic property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return selection->priv->is_italic; + } + g_free (text_content); + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "font-style"); + + if (g_strstr_len (value, -1, "italic")) + ret_val = TRUE; + else + ret_val = FALSE; + + g_free (value); + return ret_val; +} + +/** + * e_html_editor_selection_set_italic: + * @selection: an #EHTMLEditorSelection + * @italic: @TRUE to enable italic, @FALSE to disable + * + * Toggles italic formatting of current selection or letter at current cursor + * position, depending on whether @italic is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_italic (EHTMLEditorSelection *selection, + gboolean italic) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_italic (selection) == italic) + return; + + selection->priv->is_italic = italic; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_ITALIC; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "italic"); +} + +static gboolean +is_monospaced_element (WebKitDOMElement *element) +{ + gchar *value; + gboolean ret_val = FALSE; + + if (!element) + return FALSE; + + if (!WEBKIT_DOM_IS_HTML_FONT_ELEMENT (element)) + return FALSE; + + value = webkit_dom_element_get_attribute (element, "face"); + + if (g_strcmp0 (value, "monospace") == 0) + ret_val = TRUE; + + g_free (value); + + return ret_val; +} + +/** + * e_html_editor_selection_is_monospaced: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is monospaced. + * + * Returns @TRUE when selection is monospaced, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_monospaced (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + EHTMLEditorView *view; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + /* If we are changing the format of block we have to re-set italic property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return selection->priv->is_monospaced; + } + g_free (text_content); + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "font-family"); + + if (g_strstr_len (value, -1, "monospace")) + ret_val = TRUE; + else + ret_val = FALSE; + + g_free (value); + return ret_val; +} + +static void +move_caret_into_element (WebKitDOMDocument *document, + WebKitDOMElement *element) +{ + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *window_selection; + WebKitDOMRange *new_range; + + if (!element) + return; + + window = webkit_dom_document_get_default_view (document); + window_selection = webkit_dom_dom_window_get_selection (window); + new_range = webkit_dom_document_create_range (document); + + webkit_dom_range_select_node_contents ( + new_range, WEBKIT_DOM_NODE (element), NULL); + webkit_dom_range_collapse (new_range, FALSE, NULL); + webkit_dom_dom_selection_remove_all_ranges (window_selection); + webkit_dom_dom_selection_add_range (window_selection, new_range); +} + +/** + * e_html_editor_selection_set_monospaced: + * @selection: an #EHTMLEditorSelection + * @monospaced: @TRUE to enable monospaced, @FALSE to disable + * + * Toggles monospaced formatting of current selection or letter at current cursor + * position, depending on whether @monospaced is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_monospaced (EHTMLEditorSelection *selection, + gboolean monospaced) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMRange *range; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *window_selection; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_monospaced (selection) == monospaced) + return; + + selection->priv->is_monospaced = monospaced; + + range = html_editor_selection_get_current_range (selection); + if (!range) + return; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + document = webkit_web_view_get_dom_document (web_view); + window = webkit_dom_document_get_default_view (document); + window_selection = webkit_dom_dom_window_get_selection (window); + + if (monospaced) { + gchar *font_size_str; + guint font_size; + WebKitDOMElement *monospace; + + monospace = webkit_dom_document_create_element ( + document, "font", NULL); + webkit_dom_element_set_attribute ( + monospace, "face", "monospace", NULL); + + font_size = selection->priv->font_size; + if (font_size == 0) + font_size = E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL; + font_size_str = g_strdup_printf ("%d", font_size); + webkit_dom_element_set_attribute ( + monospace, "size", font_size_str, NULL); + g_free (font_size_str); + + if (g_strcmp0 (e_html_editor_selection_get_string (selection), "") != 0) { + gchar *html, *outer_html; + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (monospace), + WEBKIT_DOM_NODE ( + webkit_dom_range_clone_contents (range, NULL)), + NULL); + + outer_html = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (monospace)); + + html = g_strconcat ( + /* Mark selection for restoration */ + "<span id=\"-x-evo-selection-start-marker\"></span>", + outer_html, + "<span id=\"-x-evo-selection-end-marker\"></span>", + NULL), + + e_html_editor_selection_insert_html (selection, html); + + e_html_editor_selection_restore (selection); + + g_free (html); + g_free (outer_html); + } else { + /* https://bugs.webkit.org/show_bug.cgi?id=15256 */ + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (monospace), + UNICODE_ZERO_WIDTH_SPACE, + NULL); + webkit_dom_range_insert_node ( + range, WEBKIT_DOM_NODE (monospace), NULL); + + move_caret_into_element (document, monospace); + } + } else { + gboolean is_bold, is_italic, is_underline, is_strikethrough; + guint font_size; + WebKitDOMElement *tt_element; + WebKitDOMNode *node; + + node = webkit_dom_range_get_end_container (range, NULL); + if (WEBKIT_DOM_IS_ELEMENT (node) && + is_monospaced_element (WEBKIT_DOM_ELEMENT (node))) { + tt_element = WEBKIT_DOM_ELEMENT (node); + } else { + tt_element = e_html_editor_dom_node_find_parent_element (node, "FONT"); + + if (!is_monospaced_element (tt_element)) { + g_object_unref (view); + return; + } + } + + /* Save current formatting */ + is_bold = selection->priv->is_bold; + is_italic = selection->priv->is_italic; + is_underline = selection->priv->is_underline; + is_strikethrough = selection->priv->is_strikethrough; + font_size = selection->priv->font_size; + if (font_size == 0) + font_size = E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL; + + if (g_strcmp0 (e_html_editor_selection_get_string (selection), "") != 0) { + gchar *html, *outer_html, *inner_html, *beginning, *end; + gchar *start_position, *end_position, *font_size_str; + WebKitDOMElement *wrapper; + + wrapper = webkit_dom_document_create_element ( + document, "SPAN", NULL); + webkit_dom_element_set_id (wrapper, "-x-evo-remove-tt"); + webkit_dom_range_surround_contents ( + range, WEBKIT_DOM_NODE (wrapper), NULL); + + html = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (tt_element)); + + start_position = g_strstr_len ( + html, -1, "<span id=\"-x-evo-remove-tt\""); + end_position = g_strstr_len (start_position, -1, "</span>"); + + beginning = g_utf8_substring ( + html, 0, g_utf8_pointer_to_offset (html, start_position)); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (wrapper)); + end = g_utf8_substring ( + html, + g_utf8_pointer_to_offset (html, end_position) + 7, + g_utf8_strlen (html, -1)), + + font_size_str = g_strdup_printf ("%d", font_size); + + outer_html = + g_strconcat ( + /* Beginning */ + beginning, + /* End the previous FONT tag */ + "</font>", + /* Mark selection for restoration */ + "<span id=\"-x-evo-selection-start-marker\"></span>", + /* Inside will be the same */ + inner_html, + "<span id=\"-x-evo-selection-end-marker\"></span>", + /* Start the new FONT element */ + "<font face=\"monospace\" size=\"", + font_size_str, + "\">", + /* End - we have to start after </span> */ + end, + NULL), + + g_free (font_size_str); + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (tt_element), + outer_html, + NULL); + + e_html_editor_selection_restore (selection); + + g_free (html); + g_free (outer_html); + g_free (inner_html); + g_free (beginning); + g_free (end); + } else { + WebKitDOMRange *new_range; + gchar *outer_html; + gchar *tmp; + + webkit_dom_element_set_id (tt_element, "ev-tt"); + + outer_html = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (tt_element)); + tmp = g_strconcat (outer_html, UNICODE_ZERO_WIDTH_SPACE, NULL); + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (tt_element), + tmp, NULL); + + /* We need to get that element again */ + tt_element = webkit_dom_document_get_element_by_id ( + document, "ev-tt"); + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (tt_element), "id"); + + new_range = webkit_dom_document_create_range (document); + webkit_dom_range_set_start_after ( + new_range, WEBKIT_DOM_NODE (tt_element), NULL); + webkit_dom_range_set_end_after ( + new_range, WEBKIT_DOM_NODE (tt_element), NULL); + + webkit_dom_dom_selection_remove_all_ranges ( + window_selection); + webkit_dom_dom_selection_add_range ( + window_selection, new_range); + + webkit_dom_dom_selection_modify ( + window_selection, "move", "right", "character"); + + g_free (outer_html); + g_free (tmp); + + e_html_editor_view_force_spell_check_for_current_paragraph ( + view); + } + + /* Re-set formatting */ + if (is_bold) + e_html_editor_selection_set_bold (selection, TRUE); + if (is_italic) + e_html_editor_selection_set_italic (selection, TRUE); + if (is_underline) + e_html_editor_selection_set_underline (selection, TRUE); + if (is_strikethrough) + e_html_editor_selection_set_strikethrough (selection, TRUE); + + e_html_editor_selection_set_font_size (selection, font_size); + } + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "monospaced"); +} + +/** + * e_html_editor_selection_is_strikethrough: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is striked through. + * + * Returns @TRUE when selection is striked through, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_strikethrough (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + EHTMLEditorView *view; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + /* If we are changing the format of block we have to re-set strikethrough property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return selection->priv->is_strikethrough; + } + g_free (text_content); + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "text-decoration"); + + if (g_strstr_len (value, -1, "line-through")) + ret_val = TRUE; + else + ret_val = get_has_style (selection, "strike"); + + g_free (value); + return ret_val; +} + +/** + * e_html_editor_selection_set_strikethrough: + * @selection: an #EHTMLEditorSelection + * @strikethrough: @TRUE to enable strikethrough, @FALSE to disable + * + * Toggles strike through formatting of current selection or letter at current + * cursor position, depending on whether @strikethrough is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_strikethrough (EHTMLEditorSelection *selection, + gboolean strikethrough) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_strikethrough (selection) == strikethrough) + return; + + selection->priv->is_strikethrough = strikethrough; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_STRIKETHROUGH; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "strikethrough"); +} + +/** + * e_html_editor_selection_is_subscript: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is in subscript. + * + * Returns @TRUE when selection is in subscript, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_subscript (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMNode *node; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + g_object_unref (view); + + range = html_editor_selection_get_current_range (selection); + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + + while (node) { + gchar *tag_name; + + tag_name = webkit_dom_element_get_tag_name (WEBKIT_DOM_ELEMENT (node)); + + if (g_ascii_strncasecmp (tag_name, "sub", 3) == 0) { + g_free (tag_name); + break; + } + + g_free (tag_name); + node = webkit_dom_node_get_parent_node (node); + } + + return (node != NULL); +} + +/** + * e_html_editor_selection_set_subscript: + * @selection: an #EHTMLEditorSelection + * @subscript: @TRUE to enable subscript, @FALSE to disable + * + * Toggles subscript of current selection or letter at current cursor position, + * depending on whether @subscript is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_subscript (EHTMLEditorSelection *selection, + gboolean subscript) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_subscript (selection) == subscript) + return; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_SUBSCRIPT; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "subscript"); +} + +/** + * e_html_editor_selection_is_superscript: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is in superscript. + * + * Returns @TRUE when selection is in superscript, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_superscript (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMNode *node; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + g_object_unref (view); + + range = html_editor_selection_get_current_range (selection); + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + + while (node) { + gchar *tag_name; + + tag_name = webkit_dom_element_get_tag_name (WEBKIT_DOM_ELEMENT (node)); + + if (g_ascii_strncasecmp (tag_name, "sup", 3) == 0) { + g_free (tag_name); + break; + } + + g_free (tag_name); + node = webkit_dom_node_get_parent_node (node); + } + + return (node != NULL); +} + +/** + * e_html_editor_selection_set_superscript: + * @selection: an #EHTMLEditorSelection + * @superscript: @TRUE to enable superscript, @FALSE to disable + * + * Toggles superscript of current selection or letter at current cursor position, + * depending on whether @superscript is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_superscript (EHTMLEditorSelection *selection, + gboolean superscript) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_superscript (selection) == superscript) + return; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_SUPERSCRIPT; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "superscript"); +} + +/** + * e_html_editor_selection_is_underline: + * @selection: an #EHTMLEditorSelection + * + * Returns whether current selection or letter at current cursor position + * is underlined. + * + * Returns @TRUE when selection is underlined, @FALSE otherwise. + */ +gboolean +e_html_editor_selection_is_underline (EHTMLEditorSelection *selection) +{ + gboolean ret_val; + gchar *value, *text_content; + EHTMLEditorView *view; + WebKitDOMCSSStyleDeclaration *style; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMNode *node; + WebKitDOMElement *element; + WebKitDOMRange *range; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), FALSE); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, FALSE); + + if (!e_html_editor_view_get_html_mode (view)) { + g_object_unref (view); + return FALSE; + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + window = webkit_dom_document_get_default_view (document); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return FALSE; + + node = webkit_dom_range_get_common_ancestor_container (range, NULL); + /* If we are changing the format of block we have to re-set underline property, + * otherwise it will be turned off because of no text in composer */ + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_content, "") == 0) { + g_free (text_content); + return selection->priv->is_underline; + } + g_free (text_content); + + if (WEBKIT_DOM_IS_ELEMENT (node)) + element = WEBKIT_DOM_ELEMENT (node); + else + element = webkit_dom_node_get_parent_element (node); + + style = webkit_dom_dom_window_get_computed_style (window, element, NULL); + value = webkit_dom_css_style_declaration_get_property_value (style, "text-decoration"); + + if (g_strstr_len (value, -1, "underline")) + ret_val = TRUE; + else + ret_val = get_has_style (selection, "u"); + + g_free (value); + return ret_val; +} + +/** + * e_html_editor_selection_set_underline: + * @selection: an #EHTMLEditorSelection + * @underline: @TRUE to enable underline, @FALSE to disable + * + * Toggles underline formatting of current selection or letter at current + * cursor position, depending on whether @underline is @TRUE or @FALSE. + */ +void +e_html_editor_selection_set_underline (EHTMLEditorSelection *selection, + gboolean underline) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + if (e_html_editor_selection_is_underline (selection) == underline) + return; + + selection->priv->is_underline = underline; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_UNDERLINE; + e_html_editor_view_exec_command (view, command, NULL); + + g_object_unref (view); + + g_object_notify (G_OBJECT (selection), "underline"); +} + +/** + * e_html_editor_selection_unlink: + * @selection: an #EHTMLEditorSelection + * + * Removes any links (<A> elements) from current selection or at current + * cursor position. + */ +void +e_html_editor_selection_unlink (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range; + WebKitDOMElement *link; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + link = e_html_editor_dom_node_find_parent_element ( + webkit_dom_range_get_start_container (range, NULL), "A"); + + if (!link) { + gchar *text; + /* get element that was clicked on */ + link = e_html_editor_view_get_element_under_mouse_click (view); + if (!WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (link)) + link = NULL; + + text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (link)); + webkit_dom_html_element_set_outer_html (WEBKIT_DOM_HTML_ELEMENT (link), text, NULL); + g_free (text); + } else { + command = E_HTML_EDITOR_VIEW_COMMAND_UNLINK; + e_html_editor_view_exec_command (view, command, NULL); + } + g_object_unref (view); +} + +/** + * e_html_editor_selection_create_link: + * @selection: an #EHTMLEditorSelection + * @uri: destination of the new link + * + * Converts current selection into a link pointing to @url. + */ +void +e_html_editor_selection_create_link (EHTMLEditorSelection *selection, + const gchar *uri) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (uri != NULL && *uri != '\0'); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_CREATE_LINK; + e_html_editor_view_exec_command (view, command, uri); + + g_object_unref (view); +} + +/** + * e_html_editor_selection_insert_text: + * @selection: an #EHTMLEditorSelection + * @plain_text: text to insert + * + * Inserts @plain_text at current cursor position. When a text range is selected, + * it will be replaced by @plain_text. + */ +void +e_html_editor_selection_insert_text (EHTMLEditorSelection *selection, + const gchar *plain_text) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (plain_text != NULL); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT; + e_html_editor_view_exec_command (view, command, plain_text); + + g_object_unref (view); +} + +/** + * e_html_editor_selection_insert_html: + * @selection: an #EHTMLEditorSelection + * @html_text: an HTML code to insert + * + * Insert @html_text into document at current cursor position. When a text range + * is selected, it will be replaced by @html_text. + */ +void +e_html_editor_selection_insert_html (EHTMLEditorSelection *selection, + const gchar *html_text) +{ + EHTMLEditorView *view; + EHTMLEditorViewCommand command; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (html_text != NULL); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + command = E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML; + if (e_html_editor_view_get_html_mode (view)) { + e_html_editor_view_exec_command (view, command, html_text); + } else { + e_html_editor_view_convert_and_insert_html_to_plain_text ( + view, html_text); + } + + g_object_unref (view); +} + + +/************************* image_load_and_insert_async() *************************/ + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + EHTMLEditorSelection *selection; + WebKitDOMElement *element; + GInputStream *input_stream; + GOutputStream *output_stream; + GFile *file; + GFileInfo *file_info; + goffset total_num_bytes; + gssize bytes_read; + const gchar *content_type; + const gchar *filename; + gchar buffer[4096]; +}; + +/* Forward Declaration */ +static void +image_load_stream_read_cb (GInputStream *input_stream, + GAsyncResult *result, + LoadContext *load_context); + +static LoadContext * +image_load_context_new (EHTMLEditorSelection *selection) +{ + LoadContext *load_context; + + load_context = g_slice_new0 (LoadContext); + load_context->selection = selection; + + return load_context; +} + +static void +image_load_context_free (LoadContext *load_context) +{ + if (load_context->input_stream != NULL) + g_object_unref (load_context->input_stream); + + if (load_context->output_stream != NULL) + g_object_unref (load_context->output_stream); + + if (load_context->file_info != NULL) + g_object_unref (load_context->file_info); + + if (load_context->file != NULL) + g_object_unref (load_context->file); + + g_slice_free (LoadContext, load_context); +} + +static void +replace_base64_image_src (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + const gchar *base64_content, + const gchar *filename, + const gchar *uri) +{ + EHTMLEditorView *view; + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + e_html_editor_view_set_changed (view, TRUE); + g_object_unref (view); + + webkit_dom_html_image_element_set_src ( + WEBKIT_DOM_HTML_IMAGE_ELEMENT (element), + base64_content); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-uri", uri, NULL); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-inline", "", NULL); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-name", + filename ? filename : "", NULL); +} + +static void +insert_base64_image (EHTMLEditorSelection *selection, + const gchar *base64_content, + const gchar *filename, + const gchar *uri) +{ + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *element, *caret_position, *resizable_wrapper; + WebKitDOMText *text; + + caret_position = e_html_editor_selection_save_caret_position (selection); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + e_html_editor_view_set_changed (view, TRUE); + g_object_unref (view); + + resizable_wrapper = + webkit_dom_document_create_element (document, "span", NULL); + webkit_dom_element_set_attribute ( + resizable_wrapper, "class", "-x-evo-resizable-wrapper", NULL); + + element = webkit_dom_document_create_element (document, "img", NULL); + webkit_dom_html_image_element_set_src ( + WEBKIT_DOM_HTML_IMAGE_ELEMENT (element), + base64_content); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-uri", uri, NULL); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-inline", "", NULL); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-name", + filename ? filename : "", NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (resizable_wrapper), + WEBKIT_DOM_NODE (element), + NULL); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)), + WEBKIT_DOM_NODE (resizable_wrapper), + WEBKIT_DOM_NODE (caret_position), + NULL); + + /* We have to again use UNICODE_ZERO_WIDTH_SPACE character to restore + * caret on right position */ + text = webkit_dom_document_create_text_node ( + document, UNICODE_ZERO_WIDTH_SPACE); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)), + WEBKIT_DOM_NODE (text), + WEBKIT_DOM_NODE (caret_position), + NULL); + + e_html_editor_selection_restore_caret_position (selection); +} + +static void +image_load_finish (LoadContext *load_context) +{ + EHTMLEditorSelection *selection; + GMemoryOutputStream *output_stream; + gchar *base64_encoded, *mime_type, *output, *uri; + gsize size; + gpointer data; + + output_stream = G_MEMORY_OUTPUT_STREAM (load_context->output_stream); + + selection = load_context->selection; + + mime_type = g_content_type_get_mime_type (load_context->content_type); + + data = g_memory_output_stream_get_data (output_stream); + size = g_memory_output_stream_get_data_size (output_stream); + uri = g_file_get_uri (load_context->file); + + base64_encoded = g_base64_encode ((const guchar *) data, size); + output = g_strconcat ("data:", mime_type, ";base64,", base64_encoded, NULL); + if (load_context->element) + replace_base64_image_src ( + selection, load_context->element, output, load_context->filename, uri); + else + insert_base64_image (selection, output, load_context->filename, uri); + + g_free (base64_encoded); + g_free (output); + g_free (mime_type); + g_free (uri); + + image_load_context_free (load_context); +} + +static void +image_load_write_cb (GOutputStream *output_stream, + GAsyncResult *result, + LoadContext *load_context) +{ + GInputStream *input_stream; + gssize bytes_written; + GError *error = NULL; + + bytes_written = g_output_stream_write_finish ( + output_stream, result, &error); + + if (error) { + image_load_context_free (load_context); + return; + } + + input_stream = load_context->input_stream; + + if (bytes_written < load_context->bytes_read) { + g_memmove ( + load_context->buffer, + load_context->buffer + bytes_written, + load_context->bytes_read - bytes_written); + load_context->bytes_read -= bytes_written; + + g_output_stream_write_async ( + output_stream, + load_context->buffer, + load_context->bytes_read, + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) image_load_write_cb, + load_context); + } else + g_input_stream_read_async ( + input_stream, + load_context->buffer, + sizeof (load_context->buffer), + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) image_load_stream_read_cb, + load_context); +} + +static void +image_load_stream_read_cb (GInputStream *input_stream, + GAsyncResult *result, + LoadContext *load_context) +{ + GOutputStream *output_stream; + gssize bytes_read; + GError *error = NULL; + + bytes_read = g_input_stream_read_finish ( + input_stream, result, &error); + + if (error) { + image_load_context_free (load_context); + return; + } + + if (bytes_read == 0) { + image_load_finish (load_context); + return; + } + + output_stream = load_context->output_stream; + load_context->bytes_read = bytes_read; + + g_output_stream_write_async ( + output_stream, + load_context->buffer, + load_context->bytes_read, + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) image_load_write_cb, + load_context); +} + +static void +image_load_file_read_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + GFileInputStream *input_stream; + GOutputStream *output_stream; + GError *error = NULL; + + /* Input stream might be NULL, so don't use cast macro. */ + input_stream = g_file_read_finish (file, result, &error); + load_context->input_stream = (GInputStream *) input_stream; + + if (error) { + image_load_context_free (load_context); + return; + } + + /* Load the contents into a GMemoryOutputStream. */ + output_stream = g_memory_output_stream_new ( + NULL, 0, g_realloc, g_free); + + load_context->output_stream = output_stream; + + g_input_stream_read_async ( + load_context->input_stream, + load_context->buffer, + sizeof (load_context->buffer), + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) image_load_stream_read_cb, + load_context); +} + +static void +image_load_query_info_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + GFileInfo *file_info; + GError *error = NULL; + + file_info = g_file_query_info_finish (file, result, &error); + if (error) { + image_load_context_free (load_context); + return; + } + + load_context->content_type = g_file_info_get_content_type (file_info); + load_context->total_num_bytes = g_file_info_get_size (file_info); + load_context->filename = g_file_info_get_name (file_info); + + g_file_read_async ( + file, G_PRIORITY_DEFAULT, + NULL, (GAsyncReadyCallback) + image_load_file_read_cb, load_context); +} + +static void +image_load_and_insert_async (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + const gchar *uri) +{ + LoadContext *load_context; + GFile *file; + + g_return_if_fail (uri && *uri); + + file = g_file_new_for_uri (uri); + g_return_if_fail (file != NULL); + + load_context = image_load_context_new (selection); + load_context->file = file; + load_context->element = element; + + g_file_query_info_async ( + file, "standard::*", + G_FILE_QUERY_INFO_NONE,G_PRIORITY_DEFAULT, + NULL, (GAsyncReadyCallback) + image_load_query_info_cb, load_context); +} + +/** + * e_html_editor_selection_insert_image: + * @selection: an #EHTMLEditorSelection + * @image_uri: an URI of the source image + * + * Inserts image at current cursor position using @image_uri as source. When a + * text range is selected, it will be replaced by the image. + */ +void +e_html_editor_selection_insert_image (EHTMLEditorSelection *selection, + const gchar *image_uri) +{ + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (image_uri != NULL); + + if (is_in_html_mode (selection)) { + if (strstr (image_uri, ";base64,")) { + if (g_str_has_prefix (image_uri, "data:")) + insert_base64_image (selection, image_uri, "", ""); + if (strstr (image_uri, ";data")) { + const gchar *base64_data = strstr (image_uri, ";") + 1; + gchar *filename; + glong filename_length; + + filename_length = + g_utf8_strlen (image_uri, -1) - + g_utf8_strlen (base64_data, -1) - 1; + filename = g_strndup (image_uri, filename_length); + + insert_base64_image (selection, base64_data, filename, ""); + g_free (filename); + } + } else + image_load_and_insert_async (selection, NULL, image_uri); + } +} + +/** + * e_html_editor_selection_replace_image_src: + * @selection: an #EHTMLEditorSelection + * @image: #WebKitDOMElement representation of image + * @image_uri: an URI of the source image + * + * Replace the src attribute of the given @image with @image_uri. + */ +void +e_html_editor_selection_replace_image_src (EHTMLEditorSelection *selection, + WebKitDOMElement *image, + const gchar *image_uri) +{ + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + g_return_if_fail (image_uri != NULL); + g_return_if_fail (WEBKIT_DOM_IS_HTML_IMAGE_ELEMENT (image)); + + if (strstr (image_uri, ";base64,")) { + if (g_str_has_prefix (image_uri, "data:")) + replace_base64_image_src ( + selection, image, image_uri, "", ""); + if (strstr (image_uri, ";data")) { + const gchar *base64_data = strstr (image_uri, ";") + 1; + gchar *filename; + glong filename_length; + + filename_length = + g_utf8_strlen (image_uri, -1) - + g_utf8_strlen (base64_data, -1) - 1; + filename = g_strndup (image_uri, filename_length); + + replace_base64_image_src ( + selection, image, base64_data, filename, ""); + g_free (filename); + } + } else + image_load_and_insert_async (selection, image, image_uri); +} + +/** + * e_html_editor_selection_clear_caret_position_marker: + * @selection: an #EHTMLEditorSelection + * + * Removes previously set caret position marker from composer. + */ +void +e_html_editor_selection_clear_caret_position_marker (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *element; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + element = webkit_dom_document_get_element_by_id (document, "-x-evo-caret-position"); + + if (element) + remove_node (WEBKIT_DOM_NODE (element)); + + g_object_unref (view); +} + +WebKitDOMNode * +e_html_editor_selection_get_caret_position_node (WebKitDOMDocument *document) +{ + WebKitDOMElement *element; + + element = webkit_dom_document_create_element (document, "SPAN", NULL); + webkit_dom_element_set_id (element, "-x-evo-caret-position"); + webkit_dom_element_set_attribute ( + element, "style", "color: red", NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element), "*", NULL); + + return WEBKIT_DOM_NODE (element); +} + +/** + * e_html_editor_selection_save_caret_position: + * @selection: an #EHTMLEditorSelection + * + * Saves current caret position in composer. + * + * Returns: #WebKitDOMElement that was created on caret position + */ +WebKitDOMElement * +e_html_editor_selection_save_caret_position (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMNode *split_node; + WebKitDOMNode *start_offset_node; + WebKitDOMNode *caret_node; + WebKitDOMRange *range; + gulong start_offset; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_val_if_fail (view != NULL, NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + + e_html_editor_selection_clear_caret_position_marker (selection); + + range = html_editor_selection_get_current_range (selection); + if (!range) + return NULL; + + start_offset = webkit_dom_range_get_start_offset (range, NULL); + start_offset_node = webkit_dom_range_get_end_container (range, NULL); + + caret_node = e_html_editor_selection_get_caret_position_node (document); + + if (WEBKIT_DOM_IS_TEXT (start_offset_node) && start_offset != 0) { + WebKitDOMText *split_text; + + split_text = webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (start_offset_node), + start_offset, NULL); + split_node = WEBKIT_DOM_NODE (split_text); + } else { + split_node = start_offset_node; + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (start_offset_node), + caret_node, + split_node, + NULL); + + return WEBKIT_DOM_ELEMENT (caret_node); +} + +static void +fix_quoting_nodes_after_caret_restoration (WebKitDOMDOMSelection *window_selection, + WebKitDOMNode *prev_sibling, + WebKitDOMNode *next_sibling) +{ + WebKitDOMNode *tmp_node; + + if (!element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "-x-evo-temp-text-wrapper")) + return; + + webkit_dom_dom_selection_modify ( + window_selection, "move", "forward", "character"); + tmp_node = webkit_dom_node_get_next_sibling ( + webkit_dom_node_get_first_child (prev_sibling)); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (prev_sibling), + tmp_node, + next_sibling, + NULL); + + tmp_node = webkit_dom_node_get_first_child (prev_sibling); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (prev_sibling), + tmp_node, + webkit_dom_node_get_previous_sibling (next_sibling), + NULL); + + remove_node (prev_sibling); + + webkit_dom_dom_selection_modify ( + window_selection, "move", "backward", "character"); +} + +/** + * e_html_editor_selection_restore_caret_position: + * @selection: an #EHTMLEditorSelection + * + * Restores previously saved caret position in composer. + */ +void +e_html_editor_selection_restore_caret_position (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *element; + gboolean fix_after_quoting; + gboolean swap_direction = FALSE; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + + element = webkit_dom_document_get_element_by_id ( + document, "-x-evo-caret-position"); + fix_after_quoting = element_has_class (element, "-x-evo-caret-quoting"); + + if (element) { + WebKitDOMDOMWindow *window; + WebKitDOMNode *parent_node; + WebKitDOMDOMSelection *window_selection; + WebKitDOMNode *prev_sibling; + WebKitDOMNode *next_sibling; + + if (!webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (element))) + swap_direction = TRUE; + + window = webkit_dom_document_get_default_view (document); + window_selection = webkit_dom_dom_window_get_selection (window); + parent_node = webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)); + /* If parent is BODY element, we try to restore the position on the + * element that is next to us */ + if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent_node)) { + /* Look if we have DIV on right */ + next_sibling = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (element)); + if (!WEBKIT_DOM_IS_ELEMENT (next_sibling)) { + e_html_editor_selection_clear_caret_position_marker (selection); + return; + } + + if (element_has_class (WEBKIT_DOM_ELEMENT (next_sibling), "-x-evo-paragraph")) { + remove_node (WEBKIT_DOM_NODE (element)); + + move_caret_into_element ( + document, WEBKIT_DOM_ELEMENT (next_sibling)); + + goto out; + } + } + + move_caret_into_element (document, element); + + if (fix_after_quoting) { + prev_sibling = webkit_dom_node_get_previous_sibling ( + WEBKIT_DOM_NODE (element)); + next_sibling = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (element)); + if (!next_sibling) + fix_after_quoting = FALSE; + } + + remove_node (WEBKIT_DOM_NODE (element)); + + if (fix_after_quoting) + fix_quoting_nodes_after_caret_restoration ( + window_selection, prev_sibling, next_sibling); + out: + /* FIXME If caret position is restored and afterwards the + * position is saved it is not on the place where it supposed + * to be (it is in the beginning of parent's element. It can + * be avoided by moving with the caret. */ + if (swap_direction) { + webkit_dom_dom_selection_modify ( + window_selection, "move", "forward", "character"); + webkit_dom_dom_selection_modify ( + window_selection, "move", "backward", "character"); + } else { + webkit_dom_dom_selection_modify ( + window_selection, "move", "backward", "character"); + webkit_dom_dom_selection_modify ( + window_selection, "move", "forward", "character"); + } + } +} + +static gint +find_where_to_break_line (WebKitDOMNode *node, + gint max_len, + gint word_wrap_length) +{ + gchar *str, *text_start; + gunichar uc; + gint pos; + gint last_space = 0; + gint length; + gint ret_val = 0; + gchar* position; + + text_start = webkit_dom_character_data_get_data (WEBKIT_DOM_CHARACTER_DATA (node)); + length = g_utf8_strlen (text_start, -1); + + pos = 1; + last_space = 0; + str = text_start; + do { + uc = g_utf8_get_char (str); + if (!uc) { + g_free (text_start); + if (pos <= max_len) + return pos; + else + return last_space; + } + + /* If last_space is zero then the word is longer than + * word_wrap_length characters, so continue until we find + * a space */ + if ((pos > max_len) && (last_space > 0)) { + if (last_space > word_wrap_length) { + g_free (text_start); + return last_space; + } + + if (last_space > max_len) { + if (g_unichar_isspace (g_utf8_get_char (text_start))) + ret_val = 1; + + g_free (text_start); + return ret_val; + } + + if (last_space == max_len - 1) { + uc = g_utf8_get_char (str); + if (g_unichar_isspace (uc)) + last_space++; + } + + g_free (text_start); + return last_space; + } + + if (g_unichar_isspace (uc)) + last_space = pos; + + pos += 1; + str = g_utf8_next_char (str); + } while (*str); + + position = g_utf8_offset_to_pointer (text_start, max_len); + + if (g_unichar_isspace (g_utf8_get_char (position))) { + ret_val = max_len + 1; + } else { + if (last_space < max_len) { + ret_val = last_space; + } else { + if (length > word_wrap_length) + ret_val = last_space; + else + ret_val = 0; + } + } + + g_free (text_start); + + return ret_val; +} + +static WebKitDOMElement * +wrap_lines (EHTMLEditorSelection *selection, + WebKitDOMNode *paragraph, + WebKitDOMDocument *document, + gboolean remove_all_br, + gint word_wrap_length) +{ + WebKitDOMNode *node, *start_node; + WebKitDOMNode *paragraph_clone; + WebKitDOMDocumentFragment *fragment; + WebKitDOMElement *element; + WebKitDOMNodeList *wrap_br; + gint len, ii, br_count; + gulong length_left; + glong paragraph_char_count; + gchar *text_content; + + if (selection) { + paragraph_char_count = g_utf8_strlen ( + e_html_editor_selection_get_string (selection), -1); + + fragment = webkit_dom_range_clone_contents ( + html_editor_selection_get_current_range (selection), NULL); + + /* Select all BR elements or just ours that are used for wrapping. + * We are not removing user BR elements when this function is activated + * from Format->Wrap Lines action */ + wrap_br = webkit_dom_document_fragment_query_selector_all ( + fragment, + remove_all_br ? "br" : "br.-x-evo-wrap-br", + NULL); + } else { + WebKitDOMElement *caret_node; + + if (!webkit_dom_node_has_child_nodes (paragraph)) + return WEBKIT_DOM_ELEMENT (paragraph); + + paragraph_clone = webkit_dom_node_clone_node (paragraph, TRUE); + caret_node = webkit_dom_element_query_selector ( + WEBKIT_DOM_ELEMENT (paragraph_clone), + "span#-x-evo-caret-position", NULL); + text_content = webkit_dom_node_get_text_content (paragraph_clone); + paragraph_char_count = g_utf8_strlen (text_content, -1); + if (caret_node) + paragraph_char_count--; + g_free (text_content); + + wrap_br = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (paragraph_clone), + remove_all_br ? "br" : "br.-x-evo-wrap-br", + NULL); + } + + /* And remove them */ + br_count = webkit_dom_node_list_get_length (wrap_br); + for (ii = 0; ii < br_count; ii++) + remove_node (webkit_dom_node_list_item (wrap_br, ii)); + + if (selection) + node = WEBKIT_DOM_NODE (fragment); + else { + webkit_dom_node_normalize (paragraph_clone); + node = webkit_dom_node_get_first_child (paragraph_clone); + if (node) { + text_content = webkit_dom_node_get_text_content (node); + if (g_strcmp0 ("\n", text_content) == 0) + node = webkit_dom_node_get_next_sibling (node); + g_free (text_content); + } + } + + start_node = node; + len = 0; + while (node) { + gint offset = 0; + + if (WEBKIT_DOM_IS_TEXT (node)) { + const gchar *newline; + WebKitDOMNode *next_sibling; + + /* If there is temporary hidden space we remove it */ + text_content = webkit_dom_node_get_text_content (node); + if (strstr (text_content, UNICODE_ZERO_WIDTH_SPACE)) { + if (g_str_has_prefix (text_content, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + 0, + 1, + NULL); + if (g_str_has_suffix (text_content, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + g_utf8_strlen (text_content, -1) - 1, + 1, + NULL); + g_free (text_content); + text_content = webkit_dom_node_get_text_content (node); + } + newline = g_strstr_len (text_content, -1, "\n"); + + next_sibling = node; + while (newline) { + WebKitDOMElement *element; + + next_sibling = WEBKIT_DOM_NODE (webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (next_sibling), + g_utf8_pointer_to_offset (text_content, newline), + NULL)); + + if (!next_sibling) + break; + + element = webkit_dom_document_create_element ( + document, "BR", NULL); + element_add_class (element, "-x-evo-temp-wrap-text-br"); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (next_sibling), + WEBKIT_DOM_NODE (element), + next_sibling, + NULL); + + g_free (text_content); + + text_content = webkit_dom_node_get_text_content (next_sibling); + if (g_str_has_prefix (text_content, "\n")) { + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (next_sibling), 0, 1, NULL); + g_free (text_content); + text_content = + webkit_dom_node_get_text_content (next_sibling); + } + newline = g_strstr_len (text_content, -1, "\n"); + } + g_free (text_content); + } else { + /* If element is ANCHOR we wrap it separately */ + if (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (node)) { + glong anchor_length; + + text_content = webkit_dom_node_get_text_content (node); + anchor_length = g_utf8_strlen (text_content, -1); + if (len + anchor_length > word_wrap_length) { + element = webkit_dom_document_create_element ( + document, "BR", NULL); + element_add_class (element, "-x-evo-wrap-br"); + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + node, + NULL); + len = anchor_length; + } else + len += anchor_length; + + g_free (text_content); + node = webkit_dom_node_get_next_sibling (node); + continue; + } + + if (is_caret_position_node (node)) { + node = webkit_dom_node_get_next_sibling (node); + continue; + } + + /* When we are not removing user-entered BR elements (lines wrapped by user), + * we need to skip those elements */ + if (!remove_all_br && WEBKIT_DOM_IS_HTMLBR_ELEMENT (node)) { + if (!element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-wrap-br")) { + len = 0; + node = webkit_dom_node_get_next_sibling (node); + continue; + } + } + goto next_node; + } + + /* If length of this node + what we already have is still less + * then word_wrap_length characters, then just join it and continue to next + * node */ + length_left = webkit_dom_character_data_get_length ( + WEBKIT_DOM_CHARACTER_DATA (node)); + + if ((length_left + len) < word_wrap_length) { + len += length_left; + goto next_node; + } + + /* wrap until we have something */ + while ((length_left + len) > word_wrap_length) { + /* Find where we can line-break the node so that it + * effectively fills the rest of current row */ + offset = find_where_to_break_line ( + node, word_wrap_length - len, word_wrap_length); + + element = webkit_dom_document_create_element (document, "BR", NULL); + element_add_class (element, "-x-evo-wrap-br"); + + if (offset > 0 && offset <= word_wrap_length) { + if (offset != length_left) + webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), offset, NULL); + + if (webkit_dom_node_get_next_sibling (node)) { + gchar *nd_content; + WebKitDOMNode *nd = webkit_dom_node_get_next_sibling (node); + + nd = webkit_dom_node_get_next_sibling (node); + nd_content = webkit_dom_node_get_text_content (nd); + if (nd_content && *nd_content) { + if (g_str_has_prefix (nd_content, " ")) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (nd), 0, 1, "", NULL); + g_free (nd_content); + nd_content = webkit_dom_node_get_text_content (nd); + if (g_strcmp0 (nd_content, UNICODE_NBSP) == 0) + remove_node (nd); + g_free (nd_content); + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + nd, + NULL); + } else { + webkit_dom_node_append_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + NULL); + } + } else if (offset > word_wrap_length) { + if (offset != length_left) + webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), offset + 1, NULL); + + if (webkit_dom_node_get_next_sibling (node)) { + gchar *nd_content; + WebKitDOMNode *nd = webkit_dom_node_get_next_sibling (node); + + nd = webkit_dom_node_get_next_sibling (node); + nd_content = webkit_dom_node_get_text_content (nd); + if (nd_content && *nd_content) { + if (g_str_has_prefix (nd_content, " ")) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (nd), 0, 1, "", NULL); + g_free (nd_content); + nd_content = webkit_dom_node_get_text_content (nd); + if (g_strcmp0 (nd_content, UNICODE_NBSP) == 0) + remove_node (nd); + g_free (nd_content); + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + nd, + NULL); + } else { + webkit_dom_node_append_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + NULL); + } + len = 0; + break; + } else { + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + node, + NULL); + } + length_left = webkit_dom_character_data_get_length ( + WEBKIT_DOM_CHARACTER_DATA (node)); + + len = 0; + } + len += length_left - offset; + next_node: + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (node)) + len = 0; + + /* Move to next node */ + if (webkit_dom_node_has_child_nodes (node)) { + node = webkit_dom_node_get_first_child (node); + } else if (webkit_dom_node_get_next_sibling (node)) { + node = webkit_dom_node_get_next_sibling (node); + } else { + if (webkit_dom_node_is_equal_node (node, start_node)) + break; + + node = webkit_dom_node_get_parent_node (node); + if (node) + node = webkit_dom_node_get_next_sibling (node); + } + } + + if (selection) { + gchar *html; + + /* Create a wrapper DIV and put the processed content into it */ + element = webkit_dom_document_create_element (document, "DIV", NULL); + element_add_class (element, "-x-evo-paragraph"); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + WEBKIT_DOM_NODE (start_node), + NULL); + + webkit_dom_node_normalize (WEBKIT_DOM_NODE (element)); + /* Get HTML code of the processed content */ + html = webkit_dom_html_element_get_inner_html (WEBKIT_DOM_HTML_ELEMENT (element)); + + /* Overwrite the current selection be the processed content */ + e_html_editor_selection_insert_html (selection, html); + + g_free (html); + + return NULL; + } else { + webkit_dom_node_normalize (paragraph_clone); + + /* Replace paragraph with wrapped one */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (paragraph), + paragraph_clone, + paragraph, + NULL); + + return WEBKIT_DOM_ELEMENT (paragraph_clone); + } +} + +void +e_html_editor_selection_set_indented_style (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + gint width) +{ + EHTMLEditorSelectionAlignment alignment; + gchar *style; + const gchar *align_value; + gint word_wrap_length = (width == -1) ? selection->priv->word_wrap_length : width; + gint start = 0, end = 0; + + alignment = e_html_editor_selection_get_alignment (selection); + align_value = get_css_alignment_value (alignment); + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) + start = SPACES_PER_INDENTATION; + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) { + start = SPACES_PER_INDENTATION; + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) { + start = 0; + end = SPACES_PER_INDENTATION; + } + + webkit_dom_element_set_class_name (element, "-x-evo-indented"); + /* We don't want vertical space bellow and above blockquote inserted by + * WebKit's User Agent Stylesheet. We have to override it through style attribute. */ + if (is_in_html_mode (selection)) + style = g_strdup_printf ( + "-webkit-margin-start: %dch; -webkit-margin-end : %dch; %s", + start, end, align_value); + else + style = g_strdup_printf ( + "-webkit-margin-start: %dch; -webkit-margin-end : %dch; " + "word-wrap: normal; width: %dch; %s", + start, end, word_wrap_length, align_value); + + webkit_dom_element_set_attribute (element, "style", style, NULL); + g_free (style); +} + +WebKitDOMElement * +e_html_editor_selection_get_indented_element (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + gint width) +{ + WebKitDOMElement *element; + + element = webkit_dom_document_create_element (document, "BLOCKQUOTE", NULL); + e_html_editor_selection_set_indented_style (selection, element, width); + + return element; +} + +void +e_html_editor_selection_set_paragraph_style (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + gint width, + gint offset, + const gchar *style_to_add) +{ + EHTMLEditorSelectionAlignment alignment; + const gchar *align_value = NULL; + char *style = NULL; + gint word_wrap_length = (width == -1) ? selection->priv->word_wrap_length : width; + + alignment = e_html_editor_selection_get_alignment (selection); + align_value = get_css_alignment_value (alignment); + + element_add_class (element, "-x-evo-paragraph"); + if (!is_in_html_mode (selection)) { + style = g_strdup_printf ( + "width: %dch; word-wrap: normal; %s %s", + (word_wrap_length + offset), align_value, style_to_add); + } else { + if (*align_value || *style_to_add) + style = g_strdup_printf ( + "%s %s", align_value, style_to_add); + } + if (style) { + webkit_dom_element_set_attribute (element, "style", style, NULL); + g_free (style); + } +} + +WebKitDOMElement * +e_html_editor_selection_get_paragraph_element (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + gint width, + gint offset) +{ + WebKitDOMElement *element; + + element = webkit_dom_document_create_element (document, "DIV", NULL); + e_html_editor_selection_set_paragraph_style (selection, element, width, offset, ""); + + return element; +} + +WebKitDOMElement * +e_html_editor_selection_put_node_into_paragraph (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + WebKitDOMNode *node, + WebKitDOMNode *caret_position) +{ + WebKitDOMRange *range; + WebKitDOMElement *container; + + range = webkit_dom_document_create_range (document); + container = e_html_editor_selection_get_paragraph_element (selection, document, -1, 0); + webkit_dom_range_select_node (range, node, NULL); + webkit_dom_range_surround_contents (range, WEBKIT_DOM_NODE (container), NULL); + /* We have to move caret position inside this container */ + webkit_dom_node_append_child (WEBKIT_DOM_NODE (container), caret_position, NULL); + + return container; +} + +/** + * e_html_editor_selection_wrap_lines: + * @selection: an #EHTMLEditorSelection + * + * Wraps all lines in current selection to be 71 characters long. + */ +void +e_html_editor_selection_wrap_lines (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitDOMRange *range; + WebKitDOMDocument *document; + WebKitDOMElement *active_paragraph, *caret; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + g_object_unref (view); + + caret = e_html_editor_selection_save_caret_position (selection); + if (g_strcmp0 (e_html_editor_selection_get_string (selection), "") == 0) { + WebKitDOMNode *end_container; + WebKitDOMNode *parent; + WebKitDOMNode *paragraph; + gchar *text_content; + + /* We need to save caret position and restore it after + * wrapping the selection, but we need to save it before we + * start to modify selection */ + range = html_editor_selection_get_current_range (selection); + if (!range) + return; + + end_container = webkit_dom_range_get_common_ancestor_container (range, NULL); + + /* Wrap only text surrounded in DIV and P tags */ + parent = webkit_dom_node_get_parent_node(end_container); + if (WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent) || + WEBKIT_DOM_IS_HTML_PARAGRAPH_ELEMENT (parent)) { + element_add_class ( + WEBKIT_DOM_ELEMENT (parent), "-x-evo-paragraph"); + paragraph = parent; + } else { + WebKitDOMElement *parent_div = + e_html_editor_dom_node_find_parent_element (parent, "DIV"); + + if (element_has_class (parent_div, "-x-evo-paragraph")) { + paragraph = WEBKIT_DOM_NODE (parent_div); + } else { + if (!caret) + return; + + /* We try to select previous sibling */ + paragraph = webkit_dom_node_get_previous_sibling ( + WEBKIT_DOM_NODE (caret)); + if (paragraph) { + /* When there is just text without container + * we have to surround it with paragraph div */ + if (WEBKIT_DOM_IS_TEXT (paragraph)) + paragraph = WEBKIT_DOM_NODE ( + e_html_editor_selection_put_node_into_paragraph ( + selection, document, paragraph, + WEBKIT_DOM_NODE (caret))); + } else { + /* When some weird element is selected, return */ + e_html_editor_selection_clear_caret_position_marker (selection); + return; + } + } + } + + if (!paragraph) + return; + + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (paragraph), "style"); + webkit_dom_element_set_id ( + WEBKIT_DOM_ELEMENT (paragraph), "-x-evo-active-paragraph"); + + text_content = webkit_dom_node_get_text_content (paragraph); + /* If there is hidden space character in the beginning we remove it */ + if (strstr (text_content, UNICODE_ZERO_WIDTH_SPACE)) { + if (g_str_has_prefix (text_content, UNICODE_ZERO_WIDTH_SPACE)) { + WebKitDOMNode *node; + + node = webkit_dom_node_get_first_child (paragraph); + + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + 0, + 1, + NULL); + } + if (g_str_has_suffix (text_content, UNICODE_ZERO_WIDTH_SPACE)) { + WebKitDOMNode *node; + + node = webkit_dom_node_get_last_child (paragraph); + + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + g_utf8_strlen (text_content, -1) -1, + 1, + NULL); + } + } + g_free (text_content); + + wrap_lines ( + NULL, paragraph, document, FALSE, + selection->priv->word_wrap_length); + + } else { + e_html_editor_selection_save_caret_position (selection); + /* If we have selection -> wrap it */ + wrap_lines ( + selection, NULL, document, FALSE, + selection->priv->word_wrap_length); + } + + active_paragraph = webkit_dom_document_get_element_by_id ( + document, "-x-evo-active-paragraph"); + /* We have to move caret on position where it was before modifying the text */ + e_html_editor_selection_restore_caret_position (selection); + + /* Set paragraph as non-active */ + if (active_paragraph) + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (active_paragraph), "id"); +} + +WebKitDOMElement * +e_html_editor_selection_wrap_paragraph_length (EHTMLEditorSelection *selection, + WebKitDOMElement *paragraph, + gint length) +{ + WebKitDOMDocument *document; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + g_return_val_if_fail (WEBKIT_DOM_IS_ELEMENT (paragraph), NULL); + g_return_val_if_fail (length > 10, NULL); + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (paragraph)); + + return wrap_lines ( + NULL, WEBKIT_DOM_NODE (paragraph), document, FALSE, length); +} + +void +e_html_editor_selection_wrap_paragraphs_in_document (EHTMLEditorSelection *selection, + WebKitDOMDocument *document) +{ + WebKitDOMNodeList *list; + gint ii, length; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + list = webkit_dom_document_query_selector_all ( + document, "div.-x-evo-paragraph:not(#-x-evo-input-start)", NULL); + + length = webkit_dom_node_list_get_length (list); + + for (ii = 0; ii < length; ii++) { + gint quote, citation_level; + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + citation_level = get_citation_level (node); + quote = citation_level ? citation_level + 1 : 0; + + if (node_is_list (node)) { + WebKitDOMNode *item = webkit_dom_node_get_first_child (node); + + while (item && WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + e_html_editor_selection_wrap_paragraph_length ( + selection, + WEBKIT_DOM_ELEMENT (item), + selection->priv->word_wrap_length - quote); + item = webkit_dom_node_get_next_sibling (item); + } + } else { + e_html_editor_selection_wrap_paragraph_length ( + selection, + WEBKIT_DOM_ELEMENT (node), + selection->priv->word_wrap_length - quote); + } + } +} + +WebKitDOMElement * +e_html_editor_selection_wrap_paragraph (EHTMLEditorSelection *selection, + WebKitDOMElement *paragraph) +{ + gint indentation_level, citation_level, quote; + gint final_width, word_wrap_length, offset = 0; + + g_return_val_if_fail (E_IS_HTML_EDITOR_SELECTION (selection), NULL); + g_return_val_if_fail (WEBKIT_DOM_IS_ELEMENT (paragraph), NULL); + + word_wrap_length = selection->priv->word_wrap_length; + indentation_level = get_indentation_level (paragraph); + citation_level = get_citation_level (WEBKIT_DOM_NODE (paragraph)); + + if (node_is_list (WEBKIT_DOM_NODE (paragraph)) || + WEBKIT_DOM_IS_HTMLLI_ELEMENT (paragraph)) { + + gint list_level = get_list_level (WEBKIT_DOM_NODE (paragraph)); + indentation_level = 0; + + if (list_level > 0) + offset = list_level * -SPACES_PER_LIST_LEVEL; + else + offset = -SPACES_PER_LIST_LEVEL; + } + + quote = citation_level ? citation_level + 1 : 0; + quote *= 2; + + final_width = word_wrap_length - quote + offset; + final_width -= SPACES_PER_INDENTATION * indentation_level; + + return e_html_editor_selection_wrap_paragraph_length ( + selection, WEBKIT_DOM_ELEMENT (paragraph), final_width); +} + +/** + * e_html_editor_selection_save: + * @selection: an #EHTMLEditorSelection + * + * Saves current cursor position or current selection range. The selection can + * be later restored by calling e_html_editor_selection_restore(). + * + * Note that calling e_html_editor_selection_save() overwrites previously saved + * position. + * + * Note that this method inserts special markings into the HTML code that are + * used to later restore the selection. It can happen that by deleting some + * segments of the document some of the markings are deleted too. In that case + * restoring the selection by e_html_editor_selection_restore() can fail. Also by + * moving text segments (Cut & Paste) can result in moving the markings + * elsewhere, thus e_html_editor_selection_restore() will restore the selection + * incorrectly. + * + * It is recommended to use this method only when you are not planning to make + * bigger changes to content or structure of the document (formatting changes + * are usually OK). + */ +void +e_html_editor_selection_save (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMRange *range; + WebKitDOMNode *container; + WebKitDOMElement *marker; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + document = webkit_web_view_get_dom_document (web_view); + + /* First remove all markers (if present) */ + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-start-marker"); + if (marker != NULL) + remove_node (WEBKIT_DOM_NODE (marker)); + + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-end-marker"); + if (marker != NULL) + remove_node (WEBKIT_DOM_NODE (marker)); + + range = html_editor_selection_get_current_range (selection); + + if (range != NULL) { + WebKitDOMNode *marker_node; + WebKitDOMNode *parent_node; + WebKitDOMNode *split_node; + glong offset; + + marker = webkit_dom_document_create_element ( + document, "SPAN", NULL); + webkit_dom_element_set_id ( + marker, "-x-evo-selection-start-marker"); + + container = webkit_dom_range_get_start_container (range, NULL); + offset = webkit_dom_range_get_start_offset (range, NULL); + + if (WEBKIT_DOM_IS_TEXT (container)) { + WebKitDOMText *split_text; + + split_text = webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (container), offset, NULL); + split_node = WEBKIT_DOM_NODE (split_text); + } else if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (container)) { + webkit_dom_node_insert_before ( + container, + WEBKIT_DOM_NODE (marker), + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (container)), + NULL); + + goto end_marker; + } else { + if (!webkit_dom_node_get_previous_sibling (container)) { + split_node = webkit_dom_node_get_parent_node ( + container); + } else if (!webkit_dom_node_get_next_sibling (container)) { + split_node = webkit_dom_node_get_parent_node ( + container); + split_node = webkit_dom_node_get_next_sibling ( + split_node); + } else + split_node = container; + } + + if (!split_node) { + webkit_dom_node_insert_before ( + container, + WEBKIT_DOM_NODE (marker), + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (container)), + NULL); + } else { + marker_node = WEBKIT_DOM_NODE (marker); + parent_node = webkit_dom_node_get_parent_node (split_node); + + webkit_dom_node_insert_before ( + parent_node, marker_node, split_node, NULL); + } + end_marker: + marker = webkit_dom_document_create_element ( + document, "SPAN", NULL); + webkit_dom_element_set_id ( + marker, "-x-evo-selection-end-marker"); + + container = webkit_dom_range_get_end_container (range, NULL); + offset = webkit_dom_range_get_end_offset (range, NULL); + + if (WEBKIT_DOM_IS_TEXT (container) && offset != 0) { + WebKitDOMText *split_text; + + split_text = webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (container), offset, NULL); + split_node = WEBKIT_DOM_NODE (split_text); + } else if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (container)) { + webkit_dom_node_append_child ( + container, WEBKIT_DOM_NODE (marker), NULL); + g_object_unref (view); + return; + } else { + if (!webkit_dom_node_get_previous_sibling (container)) { + split_node = webkit_dom_node_get_parent_node ( + container); + } else if (!webkit_dom_node_get_next_sibling (container)) { + split_node = webkit_dom_node_get_parent_node ( + container); + split_node = webkit_dom_node_get_next_sibling ( + split_node); + } else + split_node = container; + } + + marker_node = WEBKIT_DOM_NODE (marker); + + if (split_node) { + parent_node = webkit_dom_node_get_parent_node (split_node); + + webkit_dom_node_insert_before ( + parent_node, marker_node, split_node, NULL); + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (container), + marker_node, + NULL); + } + } + + g_object_unref (view); +} + +/** + * e_html_editor_selection_restore: + * @selection: an #EHTMLEditorSelection + * + * Restores cursor position or selection range that was saved by + * e_html_editor_selection_save(). + * + * Note that calling this function without calling e_html_editor_selection_save() + * before is a programming error and the behavior is undefined. + */ +void +e_html_editor_selection_restore (EHTMLEditorSelection *selection) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMRange *range; + WebKitDOMElement *marker; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + document = webkit_web_view_get_dom_document (web_view); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + range = webkit_dom_document_create_range (document); + + if (range != NULL) { + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-start-marker"); + if (!marker) { + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-end-marker"); + if (marker) + remove_node (WEBKIT_DOM_NODE (marker)); + return; + } + + webkit_dom_range_set_start_after ( + range, WEBKIT_DOM_NODE (marker), NULL); + remove_node (WEBKIT_DOM_NODE (marker)); + + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-end-marker"); + if (!marker) { + marker = webkit_dom_document_get_element_by_id ( + document, "-x-evo-selection-start-marker"); + if (marker) + remove_node (WEBKIT_DOM_NODE (marker)); + return; + } + + webkit_dom_range_set_end_before ( + range, WEBKIT_DOM_NODE (marker), NULL); + remove_node (WEBKIT_DOM_NODE (marker)); + + webkit_dom_dom_selection_remove_all_ranges (dom_selection); + webkit_dom_dom_selection_add_range (dom_selection, range); + } + + g_object_unref (view); +} + +static void +html_editor_selection_modify (EHTMLEditorSelection *selection, + const gchar *alter, + gboolean forward, + EHTMLEditorSelectionGranularity granularity) +{ + EHTMLEditorView *view; + WebKitWebView *web_view; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + const gchar *granularity_str; + + g_return_if_fail (E_IS_HTML_EDITOR_SELECTION (selection)); + + view = e_html_editor_selection_ref_html_editor_view (selection); + g_return_if_fail (view != NULL); + + web_view = WEBKIT_WEB_VIEW (view); + + document = webkit_web_view_get_dom_document (web_view); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + switch (granularity) { + case E_HTML_EDITOR_SELECTION_GRANULARITY_CHARACTER: + granularity_str = "character"; + break; + case E_HTML_EDITOR_SELECTION_GRANULARITY_WORD: + granularity_str = "word"; + break; + } + + webkit_dom_dom_selection_modify ( + dom_selection, alter, + forward ? "forward" : "backward", + granularity_str); + + g_object_unref (view); +} + +/** + * e_html_editor_selection_extend: + * @selection: an #EHTMLEditorSelection + * @forward: whether to extend selection forward or backward + * @granularity: granularity of the extension + * + * Extends current selection in given direction by given granularity. + */ +void +e_html_editor_selection_extend (EHTMLEditorSelection *selection, + gboolean forward, + EHTMLEditorSelectionGranularity granularity) +{ + html_editor_selection_modify (selection, "extend", forward, granularity); +} + +/** + * e_html_editor_selection_move: + * @selection: an #EHTMLEditorSelection + * @forward: whether to move the selection forward or backward + * @granularity: granularity of the movement + * + * Moves current selection in given direction by given granularity + */ +void +e_html_editor_selection_move (EHTMLEditorSelection *selection, + gboolean forward, + EHTMLEditorSelectionGranularity granularity) +{ + html_editor_selection_modify (selection, "move", forward, granularity); +} + +void +e_html_editor_selection_scroll_to_caret (EHTMLEditorSelection *selection) +{ + WebKitDOMElement *caret; + + caret = e_html_editor_selection_save_caret_position (selection); + + webkit_dom_element_scroll_into_view (caret, TRUE); + + e_html_editor_selection_clear_caret_position_marker (selection); +} diff --git a/e-util/e-html-editor-selection.h b/e-util/e-html-editor-selection.h new file mode 100644 index 0000000000..c0d2d1817f --- /dev/null +++ b/e-util/e-html-editor-selection.h @@ -0,0 +1,250 @@ +/* + * e-html-editor-selection.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_SELECTION_H +#define E_HTML_EDITOR_SELECTION_H + +#include <gtk/gtk.h> +#include <e-util/e-util-enums.h> +#include <webkit/webkit.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_SELECTION \ + (e_html_editor_selection_get_type ()) +#define E_HTML_EDITOR_SELECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_SELECTION, EHTMLEditorSelection)) +#define E_HTML_EDITOR_SELECTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_SELECTION, EHTMLEditorSelectionClass)) +#define E_IS_HTML_EDITOR_SELECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_SELECTION)) +#define E_IS_HTML_EDITOR_SELECTION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_SELECTION)) +#define E_HTML_EDITOR_SELECTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_SELECTION, EHTMLEditorSelectionClass)) + +G_BEGIN_DECLS + +struct _EHTMLEditorView; + +typedef struct _EHTMLEditorSelection EHTMLEditorSelection; +typedef struct _EHTMLEditorSelectionClass EHTMLEditorSelectionClass; +typedef struct _EHTMLEditorSelectionPrivate EHTMLEditorSelectionPrivate; + +struct _EHTMLEditorSelection { + GObject parent; + EHTMLEditorSelectionPrivate *priv; +}; + +struct _EHTMLEditorSelectionClass { + GObjectClass parent_class; +}; + +GType e_html_editor_selection_get_type + (void) G_GNUC_CONST; +struct _EHTMLEditorView * + e_html_editor_selection_ref_html_editor_view + (EHTMLEditorSelection *selection); +void e_html_editor_selection_block_selection_changed + (EHTMLEditorSelection *selection); +void e_html_editor_selection_unblock_selection_changed + (EHTMLEditorSelection *selection); +gint e_html_editor_selection_get_word_wrap_length + (EHTMLEditorSelection *selection); +gboolean e_html_editor_selection_has_text + (EHTMLEditorSelection *selection); +gchar * e_html_editor_selection_get_caret_word + (EHTMLEditorSelection *selection); +void e_html_editor_selection_replace_caret_word + (EHTMLEditorSelection *selection, + const gchar *replacement); +EHTMLEditorSelectionAlignment + e_html_editor_selection_get_alignment + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_alignment + (EHTMLEditorSelection *selection, + EHTMLEditorSelectionAlignment alignment); +const gchar * e_html_editor_selection_get_background_color + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_background_color + (EHTMLEditorSelection *selection, + const gchar *color); +void e_html_editor_selection_get_font_color + (EHTMLEditorSelection *selection, + GdkRGBA *rgba); +void e_html_editor_selection_set_font_color + (EHTMLEditorSelection *selection, + const GdkRGBA *rgba); +const gchar * e_html_editor_selection_get_font_name + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_font_name + (EHTMLEditorSelection *selection, + const gchar *font_name); +guint e_html_editor_selection_get_font_size + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_font_size + (EHTMLEditorSelection *selection, + guint font_size); +EHTMLEditorSelectionBlockFormat + e_html_editor_selection_get_block_format + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_block_format + (EHTMLEditorSelection *selection, + EHTMLEditorSelectionBlockFormat format); +gboolean e_html_editor_selection_is_citation + (EHTMLEditorSelection *selection); +gboolean e_html_editor_selection_is_indented + (EHTMLEditorSelection *selection); +void e_html_editor_selection_indent (EHTMLEditorSelection *selection); +void e_html_editor_selection_unindent + (EHTMLEditorSelection *selection); +gboolean e_html_editor_selection_is_bold (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_bold + (EHTMLEditorSelection *selection, + gboolean bold); +gboolean e_html_editor_selection_is_italic + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_italic + (EHTMLEditorSelection *selection, + gboolean italic); +gboolean e_html_editor_selection_is_monospaced + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_monospaced + (EHTMLEditorSelection *selection, + gboolean monospaced); +gboolean e_html_editor_selection_is_strikethrough + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_strikethrough + (EHTMLEditorSelection *selection, + gboolean strikethrough); +gboolean e_html_editor_selection_is_superscript + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_superscript + (EHTMLEditorSelection *selection, + gboolean superscript); +gboolean e_html_editor_selection_is_subscript + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_subscript + (EHTMLEditorSelection *selection, + gboolean subscript); +gboolean e_html_editor_selection_is_underline + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_underline + (EHTMLEditorSelection *selection, + gboolean underline); +void e_html_editor_selection_unlink (EHTMLEditorSelection *selection); +void e_html_editor_selection_create_link + (EHTMLEditorSelection *selection, + const gchar *uri); +const gchar * e_html_editor_selection_get_string + (EHTMLEditorSelection *selection); +void e_html_editor_selection_replace (EHTMLEditorSelection *selection, + const gchar *new_string); +void e_html_editor_selection_insert_html + (EHTMLEditorSelection *selection, + const gchar *html_text); +void e_html_editor_selection_replace_image_src + (EHTMLEditorSelection *selection, + WebKitDOMElement *image, + const gchar *image_uri); +void e_html_editor_selection_insert_image + (EHTMLEditorSelection *selection, + const gchar *image_uri); +void e_html_editor_selection_insert_text + (EHTMLEditorSelection *selection, + const gchar *plain_text); +void e_html_editor_selection_clear_caret_position_marker + (EHTMLEditorSelection *selection); +WebKitDOMNode * + e_html_editor_selection_get_caret_position_node + (WebKitDOMDocument *document); +WebKitDOMElement * + e_html_editor_selection_save_caret_position + (EHTMLEditorSelection *selection); +void e_html_editor_selection_restore_caret_position + (EHTMLEditorSelection *selection); +void e_html_editor_selection_set_indented_style + (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + gint width); +WebKitDOMElement * + e_html_editor_selection_get_indented_element + (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + gint width); +void e_html_editor_selection_set_paragraph_style + (EHTMLEditorSelection *selection, + WebKitDOMElement *element, + gint width, + gint offset, + const gchar *style_to_add); +WebKitDOMElement * + e_html_editor_selection_get_paragraph_element + (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + gint width, + gint offset); +WebKitDOMElement * + e_html_editor_selection_put_node_into_paragraph + (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + WebKitDOMNode *node, + WebKitDOMNode *caret_position); +void e_html_editor_selection_wrap_lines + (EHTMLEditorSelection *selection); +WebKitDOMElement * + e_html_editor_selection_wrap_paragraph_length + (EHTMLEditorSelection *selection, + WebKitDOMElement *paragraph, + gint length); +void e_html_editor_selection_wrap_paragraphs_in_document + (EHTMLEditorSelection *selection, + WebKitDOMDocument *document); +WebKitDOMElement * + e_html_editor_selection_wrap_paragraph + (EHTMLEditorSelection *selection, + WebKitDOMElement *paragraph); +void e_html_editor_selection_save (EHTMLEditorSelection *selection); +void e_html_editor_selection_restore (EHTMLEditorSelection *selection); +void e_html_editor_selection_move (EHTMLEditorSelection *selection, + gboolean forward, + EHTMLEditorSelectionGranularity granularity); +void e_html_editor_selection_extend (EHTMLEditorSelection *selection, + gboolean forward, + EHTMLEditorSelectionGranularity granularity); +void e_html_editor_selection_scroll_to_caret + (EHTMLEditorSelection *selection); +EHTMLEditorSelectionBlockFormat + e_html_editor_selection_get_list_format_from_node + (WebKitDOMNode *node); +EHTMLEditorSelectionAlignment + e_html_editor_selection_get_list_alignment_from_node + (WebKitDOMNode *node); +G_END_DECLS + +#endif /* E_HTML_EDITOR_SELECTION_H */ diff --git a/e-util/e-html-editor-spell-check-dialog.c b/e-util/e-html-editor-spell-check-dialog.c new file mode 100644 index 0000000000..b399dfabd8 --- /dev/null +++ b/e-util/e-html-editor-spell-check-dialog.c @@ -0,0 +1,710 @@ +/* + * e-html-editor-spell-dialog.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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-spell-check-dialog.h" + +#include <glib/gi18n-lib.h> +#include <enchant/enchant.h> + +#include "e-html-editor-view.h" +#include "e-spell-checker.h" +#include "e-spell-dictionary.h" + +#define E_HTML_EDITOR_SPELL_CHECK_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG, EHTMLEditorSpellCheckDialogPrivate)) + +struct _EHTMLEditorSpellCheckDialogPrivate { + GtkWidget *add_word_button; + GtkWidget *back_button; + GtkWidget *dictionary_combo; + GtkWidget *ignore_button; + GtkWidget *replace_button; + GtkWidget *replace_all_button; + GtkWidget *skip_button; + GtkWidget *suggestion_label; + GtkWidget *tree_view; + + WebKitDOMDOMSelection *selection; + + gchar *word; + ESpellDictionary *current_dict; +}; + +enum { + COLUMN_NAME, + COLUMN_DICTIONARY, + NUM_COLUMNS +}; + +G_DEFINE_TYPE ( + EHTMLEditorSpellCheckDialog, + e_html_editor_spell_check_dialog, + E_TYPE_HTML_EDITOR_DIALOG) + +static void +html_editor_spell_check_dialog_set_word (EHTMLEditorSpellCheckDialog *dialog, + const gchar *word) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + GtkTreeView *tree_view; + GtkListStore *store; + gchar *markup; + GList *list, *link; + + if (word == NULL) + return; + + if (dialog->priv->word != word) { + g_free (dialog->priv->word); + dialog->priv->word = g_strdup (word); + } + + markup = g_strdup_printf (_("<b>Suggestions for '%s'</b>"), word); + gtk_label_set_markup ( + GTK_LABEL (dialog->priv->suggestion_label), markup); + g_free (markup); + + tree_view = GTK_TREE_VIEW (dialog->priv->tree_view); + store = GTK_LIST_STORE (gtk_tree_view_get_model (tree_view)); + gtk_list_store_clear (store); + + list = e_spell_dictionary_get_suggestions ( + dialog->priv->current_dict, word, -1); + + for (link = list; link != NULL; link = g_list_next (link)) { + GtkTreeIter iter; + gchar *suggestion = link->data; + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, suggestion, -1); + } + + g_list_free_full (list, (GDestroyNotify) g_free); + + /* We give focus to WebKit so that the currently selected word + * is highlited. Without focus selection is not visible (at + * least with my default color scheme). The focus in fact is not + * given to WebKit, because this dialog is modal, but it satisfies + * it in a way that it paints the selection :) */ + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + gtk_widget_grab_focus (GTK_WIDGET (view)); +} + +static gboolean +select_next_word (EHTMLEditorSpellCheckDialog *dialog) +{ + WebKitDOMNode *anchor, *focus; + gulong anchor_offset, focus_offset; + + anchor = webkit_dom_dom_selection_get_anchor_node (dialog->priv->selection); + anchor_offset = webkit_dom_dom_selection_get_anchor_offset (dialog->priv->selection); + + focus = webkit_dom_dom_selection_get_focus_node (dialog->priv->selection); + focus_offset = webkit_dom_dom_selection_get_focus_offset (dialog->priv->selection); + + /* Jump _behind_ next word */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "move", "forward", "word"); + /* Jump before the word */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "move", "backward", "word"); + /* Select it */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "extend", "forward", "word"); + + /* If the selection didn't change, then we have most probably + * reached the end of document - return FALSE */ + return !((anchor == webkit_dom_dom_selection_get_anchor_node ( + dialog->priv->selection)) && + (anchor_offset == webkit_dom_dom_selection_get_anchor_offset ( + dialog->priv->selection)) && + (focus == webkit_dom_dom_selection_get_focus_node ( + dialog->priv->selection)) && + (focus_offset == webkit_dom_dom_selection_get_focus_offset ( + dialog->priv->selection))); +} + +static gboolean +html_editor_spell_check_dialog_next (EHTMLEditorSpellCheckDialog *dialog) +{ + WebKitDOMNode *start = NULL, *end = NULL; + gulong start_offset, end_offset; + + if (dialog->priv->word == NULL) { + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "move", "left", "documentboundary"); + } else { + /* Remember last selected word */ + start = webkit_dom_dom_selection_get_anchor_node ( + dialog->priv->selection); + end = webkit_dom_dom_selection_get_focus_node ( + dialog->priv->selection); + start_offset = webkit_dom_dom_selection_get_anchor_offset ( + dialog->priv->selection); + end_offset = webkit_dom_dom_selection_get_focus_offset ( + dialog->priv->selection); + } + + while (select_next_word (dialog)) { + WebKitDOMRange *range; + WebKitSpellChecker *checker; + gint loc, len; + gchar *word; + + range = webkit_dom_dom_selection_get_range_at ( + dialog->priv->selection, 0, NULL); + word = webkit_dom_range_get_text (range); + + checker = WEBKIT_SPELL_CHECKER (webkit_get_text_checker ()); + webkit_spell_checker_check_spelling_of_string ( + checker, word, &loc, &len); + + /* Found misspelled word! */ + if (loc != -1) { + html_editor_spell_check_dialog_set_word (dialog, word); + g_free (word); + return TRUE; + } + + g_free (word); + } + + /* Restore the selection to contain the last misspelled word. This is + * reached only when we reach the end of the document */ + if (start && end) { + webkit_dom_dom_selection_set_base_and_extent ( + dialog->priv->selection, start, start_offset, + end, end_offset, NULL); + } + + /* Close the dialog */ + gtk_widget_hide (GTK_WIDGET (dialog)); + return FALSE; +} + +static gboolean +select_previous_word (EHTMLEditorSpellCheckDialog *dialog) +{ + WebKitDOMNode *old_anchor_node; + WebKitDOMNode *new_anchor_node; + gulong old_anchor_offset; + gulong new_anchor_offset; + + old_anchor_node = webkit_dom_dom_selection_get_anchor_node ( + dialog->priv->selection); + old_anchor_offset = webkit_dom_dom_selection_get_anchor_offset ( + dialog->priv->selection); + + /* Jump on the beginning of current word */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "move", "backward", "word"); + /* Jump before previous word */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "move", "backward", "word"); + /* Select it */ + webkit_dom_dom_selection_modify ( + dialog->priv->selection, "extend", "forward", "word"); + + /* If the selection start didn't change, then we have most probably + * reached the beginnig of document. Return FALSE */ + + new_anchor_node = webkit_dom_dom_selection_get_anchor_node ( + dialog->priv->selection); + new_anchor_offset = webkit_dom_dom_selection_get_anchor_offset ( + dialog->priv->selection); + + return (new_anchor_node != old_anchor_node) || + (new_anchor_offset != old_anchor_offset); +} + +static gboolean +html_editor_spell_check_dialog_prev (EHTMLEditorSpellCheckDialog *dialog) +{ + WebKitDOMNode *start = NULL, *end = NULL; + gulong start_offset, end_offset; + + if (dialog->priv->word == NULL) { + webkit_dom_dom_selection_modify ( + dialog->priv->selection, + "move", "right", "documentboundary"); + webkit_dom_dom_selection_modify ( + dialog->priv->selection, + "extend", "backward", "word"); + } else { + /* Remember last selected word */ + start = webkit_dom_dom_selection_get_anchor_node ( + dialog->priv->selection); + end = webkit_dom_dom_selection_get_focus_node ( + dialog->priv->selection); + start_offset = webkit_dom_dom_selection_get_anchor_offset ( + dialog->priv->selection); + end_offset = webkit_dom_dom_selection_get_focus_offset ( + dialog->priv->selection); + } + + while (select_previous_word (dialog)) { + WebKitDOMRange *range; + WebKitSpellChecker *checker; + gint loc, len; + gchar *word; + + range = webkit_dom_dom_selection_get_range_at ( + dialog->priv->selection, 0, NULL); + word = webkit_dom_range_get_text (range); + + checker = WEBKIT_SPELL_CHECKER (webkit_get_text_checker ()); + webkit_spell_checker_check_spelling_of_string ( + checker, word, &loc, &len); + + /* Found misspelled word! */ + if (loc != -1) { + html_editor_spell_check_dialog_set_word (dialog, word); + g_free (word); + return TRUE; + } + + g_free (word); + } + + /* Restore the selection to contain the last misspelled word. This is + * reached only when we reach the beginning of the document */ + if (start && end) { + webkit_dom_dom_selection_set_base_and_extent ( + dialog->priv->selection, start, start_offset, + end, end_offset, NULL); + } + + /* Close the dialog */ + gtk_widget_hide (GTK_WIDGET (dialog)); + return FALSE; +} + +static void +html_editor_spell_check_dialog_replace (EHTMLEditorSpellCheckDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *editor_selection; + GtkTreeModel *model; + GtkTreeSelection *selection; + GtkTreeIter iter; + gchar *replacement; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + editor_selection = e_html_editor_view_get_selection (view); + + selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (dialog->priv->tree_view)); + gtk_tree_selection_get_selected (selection, &model, &iter); + gtk_tree_model_get (model, &iter, 0, &replacement, -1); + + e_html_editor_selection_insert_html ( + editor_selection, replacement); + + g_free (replacement); + html_editor_spell_check_dialog_next (dialog); +} + +static void +html_editor_spell_check_dialog_replace_all (EHTMLEditorSpellCheckDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *editor_selection; + GtkTreeModel *model; + GtkTreeSelection *selection; + GtkTreeIter iter; + gchar *replacement; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + editor_selection = e_html_editor_view_get_selection (view); + + selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (dialog->priv->tree_view)); + gtk_tree_selection_get_selected (selection, &model, &iter); + gtk_tree_model_get (model, &iter, 0, &replacement, -1); + + /* Repeatedly search for 'word', then replace selection by + * 'replacement'. Repeat until there's at least one occurence of + * 'word' in the document */ + while (webkit_web_view_search_text ( + WEBKIT_WEB_VIEW (view), dialog->priv->word, + FALSE, TRUE, TRUE)) { + + e_html_editor_selection_insert_html ( + editor_selection, replacement); + } + + g_free (replacement); + html_editor_spell_check_dialog_next (dialog); +} + +static void +html_editor_spell_check_dialog_ignore (EHTMLEditorSpellCheckDialog *dialog) +{ + if (dialog->priv->word == NULL) + return; + + e_spell_dictionary_ignore_word ( + dialog->priv->current_dict, dialog->priv->word, -1); + + html_editor_spell_check_dialog_next (dialog); +} + +static void +html_editor_spell_check_dialog_learn (EHTMLEditorSpellCheckDialog *dialog) +{ + if (dialog->priv->word == NULL) + return; + + e_spell_dictionary_learn_word ( + dialog->priv->current_dict, dialog->priv->word, -1); + + html_editor_spell_check_dialog_next (dialog); +} + +static void +html_editor_spell_check_dialog_set_dictionary (EHTMLEditorSpellCheckDialog *dialog) +{ + GtkComboBox *combo_box; + GtkTreeModel *model; + GtkTreeIter iter; + ESpellDictionary *dictionary; + + combo_box = GTK_COMBO_BOX (dialog->priv->dictionary_combo); + gtk_combo_box_get_active_iter (combo_box, &iter); + model = gtk_combo_box_get_model (combo_box); + + gtk_tree_model_get (model, &iter, 1, &dictionary, -1); + + dialog->priv->current_dict = dictionary; + + /* Update suggestions */ + html_editor_spell_check_dialog_set_word (dialog, dialog->priv->word); +} + +static void +html_editor_spell_check_dialog_show (GtkWidget *widget) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSpellCheckDialog *dialog; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + + dialog = E_HTML_EDITOR_SPELL_CHECK_DIALOG (widget); + + g_free (dialog->priv->word); + dialog->priv->word = NULL; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dialog->priv->selection = webkit_dom_dom_window_get_selection (window); + + /* Select the first word or quit */ + if (html_editor_spell_check_dialog_next (dialog)) { + GTK_WIDGET_CLASS (e_html_editor_spell_check_dialog_parent_class)-> + show (widget); + } +} + +static void +html_editor_spell_check_dialog_finalize (GObject *object) +{ + EHTMLEditorSpellCheckDialogPrivate *priv; + + priv = E_HTML_EDITOR_SPELL_CHECK_DIALOG_GET_PRIVATE (object); + + g_free (priv->word); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_html_editor_spell_check_dialog_parent_class)-> + finalize (object); +} + +static void +html_editor_spell_check_dialog_constructed (GObject *object) +{ + EHTMLEditorSpellCheckDialog *dialog; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_html_editor_spell_check_dialog_parent_class)-> + constructed (object); + + dialog = E_HTML_EDITOR_SPELL_CHECK_DIALOG (object); + e_html_editor_spell_check_dialog_update_dictionaries (dialog); +} + +static void +e_html_editor_spell_check_dialog_class_init (EHTMLEditorSpellCheckDialogClass *class) +{ + GtkWidgetClass *widget_class; + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EHTMLEditorSpellCheckDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = html_editor_spell_check_dialog_finalize; + object_class->constructed = html_editor_spell_check_dialog_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_spell_check_dialog_show; +} + +static void +e_html_editor_spell_check_dialog_init (EHTMLEditorSpellCheckDialog *dialog) +{ + GtkWidget *widget; + GtkGrid *main_layout; + GtkListStore *store; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + + dialog->priv = E_HTML_EDITOR_SPELL_CHECK_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == Suggestions == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Suggestions</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 2, 1); + dialog->priv->suggestion_label = widget; + + /* Tree view */ + widget = gtk_tree_view_new (); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (widget), FALSE); + gtk_widget_set_vexpand (widget, TRUE); + gtk_widget_set_hexpand (widget, TRUE); + dialog->priv->tree_view = widget; + + /* Column */ + column = gtk_tree_view_column_new_with_attributes ( + "", gtk_cell_renderer_text_new (), "text", 0, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (widget), column); + + /* Store */ + store = gtk_list_store_new (1, G_TYPE_STRING); + gtk_tree_view_set_model ( + GTK_TREE_VIEW (widget), GTK_TREE_MODEL (store)); + + /* Scrolled Window */ + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_size_request (widget, 150, -1); + 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_ETCHED_IN); + gtk_container_add (GTK_CONTAINER (widget), dialog->priv->tree_view); + gtk_grid_attach (main_layout, widget, 0, 1, 1, 5); + + /* Replace */ + widget = gtk_button_new_with_mnemonic (_("Replace")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_CONVERT, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 1, 1, 1); + dialog->priv->replace_button = widget; + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_replace), dialog); + + /* Replace All */ + widget = gtk_button_new_with_mnemonic (_("Replace All")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_APPLY, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 2, 1, 1); + dialog->priv->replace_all_button = widget; + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_replace_all), dialog); + + /* Ignore */ + widget = gtk_button_new_with_mnemonic (_("Ignore")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_CLEAR, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 3, 1, 1); + dialog->priv->ignore_button = widget; + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_ignore), dialog); + + /* Skip */ + widget = gtk_button_new_with_mnemonic (_("Skip")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_GO_FORWARD, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 4, 1, 1); + dialog->priv->skip_button = widget; + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_next), dialog); + + /* Back */ + widget = gtk_button_new_with_mnemonic (_("Back")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_GO_BACK, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 5, 1, 1); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_prev), dialog); + + /* Dictionary label */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Dictionary</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0); + gtk_grid_attach (main_layout, widget, 0, 6, 2, 1); + + /* Dictionaries combo */ + widget = gtk_combo_box_new (); + gtk_grid_attach (main_layout, widget, 0, 7, 1, 1); + dialog->priv->dictionary_combo = widget; + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (widget), renderer, TRUE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (widget), renderer, "text", 0); + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_spell_check_dialog_set_dictionary), dialog); + + /* Add Word button */ + widget = gtk_button_new_with_mnemonic (_("Add Word")); + gtk_button_set_image ( + GTK_BUTTON (widget), + gtk_image_new_from_stock ( + GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON)); + gtk_grid_attach (main_layout, widget, 1, 7, 1, 1); + dialog->priv->add_word_button = widget; + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (html_editor_spell_check_dialog_learn), dialog); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_spell_check_dialog_new (EHTMLEditor *editor) +{ + return g_object_new ( + E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG, + "editor", editor, + "title", N_("Spell Checking"), + NULL); +} + +void +e_html_editor_spell_check_dialog_update_dictionaries (EHTMLEditorSpellCheckDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + ESpellChecker *spell_checker; + GtkComboBox *combo_box; + GtkListStore *store; + GQueue queue = G_QUEUE_INIT; + gchar **languages; + guint n_languages = 0; + guint ii; + + g_return_if_fail (E_IS_HTML_EDITOR_SPELL_CHECK_DIALOG (dialog)); + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + 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, &n_languages); + for (ii = 0; ii < n_languages; ii++) { + ESpellDictionary *dictionary; + + dictionary = e_spell_checker_ref_dictionary ( + spell_checker, languages[ii]); + if (dictionary != NULL) + g_queue_push_tail (&queue, dictionary); + else + g_warning ( + "%s: No '%s' dictionary found", + G_STRFUNC, languages[ii]); + } + g_strfreev (languages); + + /* Populate a list store for the combo box. */ + store = gtk_list_store_new ( + NUM_COLUMNS, + G_TYPE_STRING, /* COLUMN_NAME */ + E_TYPE_SPELL_DICTIONARY); /* COLUMN_DICTIONARY */ + + while (!g_queue_is_empty (&queue)) { + ESpellDictionary *dictionary; + GtkTreeIter iter; + const gchar *name; + + dictionary = g_queue_pop_head (&queue); + name = e_spell_dictionary_get_name (dictionary); + + gtk_list_store_append (store, &iter); + gtk_list_store_set ( + store, &iter, + COLUMN_NAME, name, + COLUMN_DICTIONARY, dictionary, + -1); + + g_object_unref (dictionary); + } + + /* FIXME Try to restore selection. */ + combo_box = GTK_COMBO_BOX (dialog->priv->dictionary_combo); + gtk_combo_box_set_model (combo_box, GTK_TREE_MODEL (store)); + gtk_combo_box_set_active (combo_box, 0); + + g_object_unref (store); +} + diff --git a/e-util/e-html-editor-spell-check-dialog.h b/e-util/e-html-editor-spell-check-dialog.h new file mode 100644 index 0000000000..4191cd98ad --- /dev/null +++ b/e-util/e-html-editor-spell-check-dialog.h @@ -0,0 +1,73 @@ +/* + * e-html-editor-spell-check-dialog.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_SPELL_CHECK_DIALOG_H +#define E_HTML_EDITOR_SPELL_CHECK_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG \ + (e_html_editor_spell_check_dialog_get_type ()) +#define E_HTML_EDITOR_SPELL_CHECK_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG, EHTMLEditorSpellCheckDialog)) +#define E_HTML_EDITOR_SPELL_CHECK_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG, EHTMLEditorSpellCheckDialogClass)) +#define E_IS_HTML_EDITOR_SPELL_CHECK_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG)) +#define E_IS_HTML_EDITOR_SPELL_CHECK_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG)) +#define E_HTML_EDITOR_SPELL_CHECK_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_SPELL_CHECK_DIALOG, EHTMLEditorSpellCheckDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorSpellCheckDialog EHTMLEditorSpellCheckDialog; +typedef struct _EHTMLEditorSpellCheckDialogClass EHTMLEditorSpellCheckDialogClass; +typedef struct _EHTMLEditorSpellCheckDialogPrivate EHTMLEditorSpellCheckDialogPrivate; + +struct _EHTMLEditorSpellCheckDialog { + EHTMLEditorDialog parent; + EHTMLEditorSpellCheckDialogPrivate *priv; +}; + +struct _EHTMLEditorSpellCheckDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_spell_check_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_html_editor_spell_check_dialog_new + (EHTMLEditor *editor); +void e_html_editor_spell_check_dialog_update_dictionaries + (EHTMLEditorSpellCheckDialog *dialog); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_SPELL_CHECK_DIALOG_H */ diff --git a/e-util/e-html-editor-table-dialog.c b/e-util/e-html-editor-table-dialog.c new file mode 100644 index 0000000000..467d2a62e8 --- /dev/null +++ b/e-util/e-html-editor-table-dialog.c @@ -0,0 +1,866 @@ +/* + * e-html-editor-table-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-table-dialog.h" + +#include <glib/gi18n-lib.h> + +#include "e-color-combo.h" +#include "e-html-editor-utils.h" +#include "e-image-chooser-dialog.h" +#include "e-misc-utils.h" + +#define E_HTML_EDITOR_TABLE_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_TABLE_DIALOG, EHTMLEditorTableDialogPrivate)) + +struct _EHTMLEditorTableDialogPrivate { + GtkWidget *rows_edit; + GtkWidget *columns_edit; + + GtkWidget *width_edit; + GtkWidget *width_units; + GtkWidget *width_check; + + GtkWidget *spacing_edit; + GtkWidget *padding_edit; + GtkWidget *border_edit; + + GtkWidget *alignment_combo; + + GtkWidget *background_color_button; + GtkWidget *background_image_button; + GtkWidget *image_chooser_dialog; + + WebKitDOMHTMLTableElement *table_element; +}; + +static GdkRGBA white = { 1, 1, 1, 1 }; + +G_DEFINE_TYPE ( + EHTMLEditorTableDialog, + e_html_editor_table_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static WebKitDOMElement * +html_editor_table_dialog_create_table (EHTMLEditorTableDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorSelection *editor_selection; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *table, *br, *caret, *parent, *element; + gint i; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + editor_selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + /* Default 3x3 table */ + table = webkit_dom_document_create_element (document, "TABLE", NULL); + for (i = 0; i < 3; i++) { + WebKitDOMHTMLElement *row; + gint j; + + row = webkit_dom_html_table_element_insert_row ( + WEBKIT_DOM_HTML_TABLE_ELEMENT (table), -1, NULL); + + for (j = 0; j < 3; j++) { + webkit_dom_html_table_row_element_insert_cell ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row), -1, NULL); + } + } + + caret = e_html_editor_selection_save_caret_position (editor_selection); + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (caret)); + element = caret; + + while (!WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + element = parent; + parent = webkit_dom_node_get_parent_element ( + WEBKIT_DOM_NODE (parent)); + } + + br = webkit_dom_document_create_element (document, "BR", NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (br), + webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (element)), + NULL); + + /* Insert the table into body below the caret */ + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (table), + webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (element)), + NULL); + + e_html_editor_selection_clear_caret_position_marker (editor_selection); + + e_html_editor_view_set_changed (view, TRUE); + + return table; +} + +static void +html_editor_table_dialog_set_row_count (EHTMLEditorTableDialog *dialog) +{ + WebKitDOMHTMLCollection *rows; + gulong ii, current_count, expected_count; + + g_return_if_fail (dialog->priv->table_element); + + rows = webkit_dom_html_table_element_get_rows (dialog->priv->table_element); + current_count = webkit_dom_html_collection_get_length (rows); + expected_count = gtk_spin_button_get_value ( + GTK_SPIN_BUTTON (dialog->priv->rows_edit)); + + if (current_count < expected_count) { + for (ii = 0; ii < expected_count - current_count; ii++) { + webkit_dom_html_table_element_insert_row ( + dialog->priv->table_element, -1, NULL); + } + } else if (current_count > expected_count) { + for (ii = 0; ii < current_count - expected_count; ii++) { + webkit_dom_html_table_element_delete_row ( + dialog->priv->table_element, -1, NULL); + } + } +} + +static void +html_editor_table_dialog_get_row_count (EHTMLEditorTableDialog *dialog) +{ + WebKitDOMHTMLCollection *rows; + + g_return_if_fail (dialog->priv->table_element); + + rows = webkit_dom_html_table_element_get_rows (dialog->priv->table_element); + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->rows_edit), + webkit_dom_html_collection_get_length (rows)); +} + +static void +html_editor_table_dialog_set_column_count (EHTMLEditorTableDialog *dialog) +{ + WebKitDOMHTMLCollection *rows; + gulong ii, row_count, expected_columns; + + g_return_if_fail (dialog->priv->table_element); + + rows = webkit_dom_html_table_element_get_rows (dialog->priv->table_element); + row_count = webkit_dom_html_collection_get_length (rows); + expected_columns = gtk_spin_button_get_value ( + GTK_SPIN_BUTTON (dialog->priv->columns_edit)); + + for (ii = 0; ii < row_count; ii++) { + WebKitDOMHTMLTableRowElement *row; + WebKitDOMHTMLCollection *cells; + gulong jj, current_columns; + + row = WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT ( + webkit_dom_html_collection_item (rows, ii)); + + cells = webkit_dom_html_table_row_element_get_cells (row); + current_columns = webkit_dom_html_collection_get_length (cells); + + if (current_columns < expected_columns) { + for (jj = 0; jj < expected_columns - current_columns; jj++) { + webkit_dom_html_table_row_element_insert_cell ( + row, -1, NULL); + } + } else if (expected_columns < current_columns) { + for (jj = 0; jj < current_columns - expected_columns; jj++) { + webkit_dom_html_table_row_element_delete_cell ( + row, -1, NULL); + } + } + } +} + +static void +html_editor_table_dialog_get_column_count (EHTMLEditorTableDialog *dialog) +{ + WebKitDOMHTMLCollection *rows, *columns; + WebKitDOMNode *row; + + g_return_if_fail (dialog->priv->table_element); + + rows = webkit_dom_html_table_element_get_rows (dialog->priv->table_element); + row = webkit_dom_html_collection_item (rows, 0); + + columns = webkit_dom_html_table_row_element_get_cells ( + WEBKIT_DOM_HTML_TABLE_ROW_ELEMENT (row)); + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->columns_edit), + webkit_dom_html_collection_get_length (columns)); +} + +static void +html_editor_table_dialog_set_width (EHTMLEditorTableDialog *dialog) +{ + gchar *width; + + g_return_if_fail (dialog->priv->table_element); + + if (gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check))) { + gchar *units; + + units = gtk_combo_box_text_get_active_text ( + GTK_COMBO_BOX_TEXT (dialog->priv->width_units)); + width = g_strdup_printf ( + "%d%s", + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->width_edit)), + units); + g_free (units); + + gtk_widget_set_sensitive (dialog->priv->width_edit, TRUE); + gtk_widget_set_sensitive (dialog->priv->width_units, TRUE); + } else { + width = g_strdup ("auto"); + + gtk_widget_set_sensitive (dialog->priv->width_edit, FALSE); + gtk_widget_set_sensitive (dialog->priv->width_units, FALSE); + } + + webkit_dom_html_table_element_set_width ( + dialog->priv->table_element, width); + g_free (width); +} + +static void +html_editor_table_dialog_get_width (EHTMLEditorTableDialog *dialog) +{ + gchar *width; + + width = webkit_dom_html_table_element_get_width (dialog->priv->table_element); + if (!width || !*width || g_ascii_strncasecmp (width, "auto", 4) == 0) { + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check), FALSE); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), 100); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->width_units), "units-percent"); + } else { + gint width_int = atoi (width); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check), TRUE); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), width_int); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->width_units), + ((strstr (width, "%") == NULL) ? + "units-px" : "units-percent")); + } + g_free (width); +} + +static void +html_editor_table_dialog_set_alignment (EHTMLEditorTableDialog *dialog) +{ + g_return_if_fail (dialog->priv->table_element); + + webkit_dom_html_table_element_set_align ( + dialog->priv->table_element, + gtk_combo_box_get_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo))); +} + +static void +html_editor_table_dialog_get_alignment (EHTMLEditorTableDialog *dialog) +{ + gchar *alignment; + + g_return_if_fail (dialog->priv->table_element); + + alignment = webkit_dom_html_table_element_get_align ( + dialog->priv->table_element); + + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo), alignment); + + g_free (alignment); +} + +static void +html_editor_table_dialog_set_padding (EHTMLEditorTableDialog *dialog) +{ + gchar *padding; + + g_return_if_fail (dialog->priv->table_element); + + padding = g_strdup_printf ( + "%d", + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->padding_edit))); + + webkit_dom_html_table_element_set_cell_padding ( + dialog->priv->table_element, padding); + + g_free (padding); +} + +static void +html_editor_table_dialog_get_padding (EHTMLEditorTableDialog *dialog) +{ + gchar *padding; + gint padding_int; + + g_return_if_fail (dialog->priv->table_element); + + padding = webkit_dom_html_table_element_get_cell_padding ( + dialog->priv->table_element); + if (!padding || !*padding) { + padding_int = 0; + } else { + padding_int = atoi (padding); + } + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->padding_edit), padding_int); + + g_free (padding); +} + +static void +html_editor_table_dialog_set_spacing (EHTMLEditorTableDialog *dialog) +{ + gchar *spacing; + + g_return_if_fail (dialog->priv->table_element); + + spacing = g_strdup_printf ( + "%d", + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->spacing_edit))); + + webkit_dom_html_table_element_set_cell_spacing ( + dialog->priv->table_element, spacing); + + g_free (spacing); +} + +static void +html_editor_table_dialog_get_spacing (EHTMLEditorTableDialog *dialog) +{ + gchar *spacing; + gint spacing_int; + + g_return_if_fail (dialog->priv->table_element); + + spacing = webkit_dom_html_table_element_get_cell_spacing ( + dialog->priv->table_element); + if (!spacing || !*spacing) { + spacing_int = 0; + } else { + spacing_int = atoi (spacing); + } + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->spacing_edit), spacing_int); + + g_free (spacing); +} + +static void +html_editor_table_dialog_set_border (EHTMLEditorTableDialog *dialog) +{ + gchar *border; + + g_return_if_fail (dialog->priv->table_element); + + border = g_strdup_printf ( + "%d", + gtk_spin_button_get_value_as_int ( + GTK_SPIN_BUTTON (dialog->priv->border_edit))); + + webkit_dom_html_table_element_set_border ( + dialog->priv->table_element, border); + + g_free (border); +} + +static void +html_editor_table_dialog_get_border (EHTMLEditorTableDialog *dialog) +{ + gchar *border; + gint border_int; + + g_return_if_fail (dialog->priv->table_element); + + border = webkit_dom_html_table_element_get_border ( + dialog->priv->table_element); + if (!border || !*border) { + border_int = 0; + } else { + border_int = atoi (border); + } + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->border_edit), border_int); + + g_free (border); +} + +static void +html_editor_table_dialog_set_background_color (EHTMLEditorTableDialog *dialog) +{ + gchar *color; + GdkRGBA rgba; + + g_return_if_fail (dialog->priv->table_element); + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_button), &rgba); + color = g_strdup_printf ( + "#%06x", e_rgba_to_value (&rgba)); + + webkit_dom_html_table_element_set_bg_color ( + dialog->priv->table_element, color); + + g_free (color); +} + +static void +html_editor_table_dialog_get_background_color (EHTMLEditorTableDialog *dialog) +{ + gchar *color; + GdkRGBA rgba; + + g_return_if_fail (dialog->priv->table_element); + + color = webkit_dom_html_table_element_get_bg_color ( + dialog->priv->table_element); + + gdk_rgba_parse (&rgba, color); + + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_button), &rgba); + + g_free (color); +} + +static void +html_editor_table_dialog_set_background_image (EHTMLEditorTableDialog *dialog) +{ + const gchar *filename; + + g_return_if_fail (dialog->priv->table_element); + + filename = gtk_file_chooser_get_filename ( + GTK_FILE_CHOOSER (dialog->priv->background_image_button)); + + if (filename) { + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->table_element), + "background", filename, NULL); + } else { + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->table_element), + "background"); + } +} + +static void +html_editor_table_dialog_get_background_image (EHTMLEditorTableDialog *dialog) +{ + g_return_if_fail (dialog->priv->table_element); + + if (!webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->table_element), "background")) { + + gtk_file_chooser_unselect_all ( + GTK_FILE_CHOOSER (dialog->priv->background_image_button)); + return; + } else { + gchar *background; + + background = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (dialog->priv->table_element), "background"); + + gtk_file_chooser_set_filename ( + GTK_FILE_CHOOSER (dialog->priv->background_image_button), + background); + + g_free (background); + } +} + +static void +html_editor_table_dialog_get_values (EHTMLEditorTableDialog *dialog) +{ + html_editor_table_dialog_get_row_count (dialog); + html_editor_table_dialog_get_column_count (dialog); + html_editor_table_dialog_get_width (dialog); + html_editor_table_dialog_get_alignment (dialog); + html_editor_table_dialog_get_spacing (dialog); + html_editor_table_dialog_get_padding (dialog); + html_editor_table_dialog_get_border (dialog); + html_editor_table_dialog_get_background_color (dialog); + html_editor_table_dialog_get_background_image (dialog); +} + +static void +html_editor_table_dialog_reset_values (EHTMLEditorTableDialog *dialog) +{ + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->rows_edit), 3); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->columns_edit), 3); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->alignment_combo), "left"); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->width_check), TRUE); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->width_edit), 100); + gtk_combo_box_set_active_id ( + GTK_COMBO_BOX (dialog->priv->width_units), "units-percent"); + + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->spacing_edit), 2); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->padding_edit), 1); + gtk_spin_button_set_value ( + GTK_SPIN_BUTTON (dialog->priv->border_edit), 1); + + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->background_color_button), &white); + gtk_file_chooser_unselect_all ( + GTK_FILE_CHOOSER (dialog->priv->background_image_button)); + + html_editor_table_dialog_set_row_count (dialog); + html_editor_table_dialog_set_column_count (dialog); + html_editor_table_dialog_set_width (dialog); + html_editor_table_dialog_set_alignment (dialog); + html_editor_table_dialog_set_spacing (dialog); + html_editor_table_dialog_set_padding (dialog); + html_editor_table_dialog_set_border (dialog); + html_editor_table_dialog_set_background_color (dialog); + html_editor_table_dialog_set_background_image (dialog); +} + +static void +html_editor_table_dialog_show (GtkWidget *widget) +{ + EHTMLEditorTableDialog *dialog; + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *selection; + + dialog = E_HTML_EDITOR_TABLE_DIALOG (widget); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + selection = webkit_dom_dom_window_get_selection (window); + if (selection && (webkit_dom_dom_selection_get_range_count (selection) > 0)) { + WebKitDOMElement *table; + WebKitDOMRange *range; + + range = webkit_dom_dom_selection_get_range_at (selection, 0, NULL); + table = e_html_editor_dom_node_find_parent_element ( + webkit_dom_range_get_start_container (range, NULL), "TABLE"); + + if (!table) { + dialog->priv->table_element = WEBKIT_DOM_HTML_TABLE_ELEMENT ( + html_editor_table_dialog_create_table (dialog)); + html_editor_table_dialog_reset_values (dialog); + } else { + dialog->priv->table_element = + WEBKIT_DOM_HTML_TABLE_ELEMENT (table); + html_editor_table_dialog_get_values (dialog); + } + } + + /* Chain up to parent implementation */ + GTK_WIDGET_CLASS (e_html_editor_table_dialog_parent_class)->show (widget); +} + +static void +html_editor_table_dialog_hide (GtkWidget *widget) +{ + EHTMLEditorTableDialogPrivate *priv; + + priv = E_HTML_EDITOR_TABLE_DIALOG_GET_PRIVATE (widget); + + priv->table_element = NULL; + + GTK_WIDGET_CLASS (e_html_editor_table_dialog_parent_class)->hide (widget); +} + +static void +e_html_editor_table_dialog_class_init (EHTMLEditorTableDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorTableDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_table_dialog_show; + widget_class->hide = html_editor_table_dialog_hide; +} + +static void +e_html_editor_table_dialog_init (EHTMLEditorTableDialog *dialog) +{ + GtkGrid *main_layout, *grid; + GtkWidget *widget; + GtkFileFilter *file_filter; + + dialog->priv = E_HTML_EDITOR_TABLE_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* == General == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>General</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 1, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Rows */ + widget = gtk_image_new_from_icon_name ("stock_select-row", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + widget = gtk_spin_button_new_with_range (1, G_MAXINT, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_row_count), dialog); + dialog->priv->rows_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Rows:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->rows_edit); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + + /* Columns */ + widget = gtk_image_new_from_icon_name ("stock_select-column", GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (grid, widget, 3, 0, 1, 1); + + widget = gtk_spin_button_new_with_range (1, G_MAXINT, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 5, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_column_count), dialog); + dialog->priv->columns_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("C_olumns:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->columns_edit); + gtk_grid_attach (grid, widget, 4, 0, 1, 1); + + /* == Layout == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Layout</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 3, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Width */ + widget = gtk_check_button_new_with_mnemonic (_("_Width:")); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_table_dialog_set_width), dialog); + dialog->priv->width_check = widget; + + widget = gtk_spin_button_new_with_range (1, 100, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_width), dialog); + dialog->priv->width_edit = widget; + + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-px", "px"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "units-percent", "%"); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_table_dialog_set_width), dialog); + dialog->priv->width_units = widget; + + /* Spacing */ + widget = gtk_spin_button_new_with_range (0, G_MAXINT, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 5, 0, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_spacing), dialog); + dialog->priv->spacing_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Spacing:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->spacing_edit); + gtk_grid_attach (grid, widget, 4, 0, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 0, 1, 1); + + /* Padding */ + widget = gtk_spin_button_new_with_range (0, G_MAXINT, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 5, 1, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_padding), dialog); + dialog->priv->padding_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Padding:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->padding_edit); + gtk_grid_attach (grid, widget, 4, 1, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 1, 1, 1); + + /* Border */ + widget = gtk_spin_button_new_with_range (0, G_MAXINT, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (widget), 0); + gtk_grid_attach (grid, widget, 5, 2, 1, 1); + g_signal_connect_swapped ( + widget, "value-changed", + G_CALLBACK (html_editor_table_dialog_set_border), dialog); + dialog->priv->border_edit = widget; + + widget = gtk_label_new_with_mnemonic (_("_Border:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->border_edit); + gtk_grid_attach (grid, widget, 4, 2, 1, 1); + + widget = gtk_label_new ("px"); + gtk_grid_attach (grid, widget, 6, 2, 1, 1); + + /* Alignment */ + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "left", _("Left")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "center", _("Center")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "right", _("Right")); + gtk_grid_attach (grid, widget, 1, 1, 2, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_table_dialog_set_alignment), dialog); + dialog->priv->alignment_combo = widget; + + widget = gtk_label_new_with_mnemonic (_("_Alignment:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->alignment_combo); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + /* == Background == */ + widget = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (widget), _("<b>Background</b>")); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_grid_attach (main_layout, widget, 0, 4, 1, 1); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 5); + gtk_grid_set_column_spacing (grid, 5); + gtk_grid_attach (main_layout, GTK_WIDGET (grid), 0, 5, 1, 1); + gtk_widget_set_margin_left (GTK_WIDGET (grid), 10); + + /* Color */ + widget = e_color_combo_new (); + e_color_combo_set_default_color (E_COLOR_COMBO (widget), &white); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_table_dialog_set_background_color), dialog); + dialog->priv->background_color_button = widget; + + widget = gtk_label_new_with_mnemonic (_("_Color:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_color_button); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + /* Image */ + widget = e_image_chooser_dialog_new ( + _("Choose Background Image"), + GTK_WINDOW (dialog)); + dialog->priv->image_chooser_dialog = widget; + + file_filter = gtk_file_filter_new (); + gtk_file_filter_set_name (file_filter, _("Images")); + gtk_file_filter_add_mime_type (file_filter, "image/*"); + + widget = gtk_file_chooser_button_new_with_dialog ( + dialog->priv->image_chooser_dialog); + gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (widget), file_filter); + gtk_widget_set_hexpand (widget, TRUE); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + g_signal_connect_swapped ( + widget, "file-set", + G_CALLBACK (html_editor_table_dialog_set_background_image), dialog); + dialog->priv->background_image_button = widget; + + widget =gtk_label_new_with_mnemonic (_("Image:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget ( + GTK_LABEL (widget), dialog->priv->background_image_button); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_table_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_TABLE_DIALOG, + "editor", editor, + "title", N_("Table Properties"), + NULL)); +} + diff --git a/e-util/e-html-editor-table-dialog.h b/e-util/e-html-editor-table-dialog.h new file mode 100644 index 0000000000..70c790d22d --- /dev/null +++ b/e-util/e-html-editor-table-dialog.h @@ -0,0 +1,69 @@ +/* + * e-html-editor-table-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_TABLE_DIALOG_H +#define E_HTML_EDITOR_TABLE_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_TABLE_DIALOG \ + (e_html_editor_table_dialog_get_type ()) +#define E_HTML_EDITOR_TABLE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_TABLE_DIALOG, EHTMLEditorTableDialog)) +#define E_HTML_EDITOR_TABLE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_TABLE_DIALOG, EHTMLEditorTableDialogClass)) +#define E_IS_HTML_EDITOR_TABLE_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_TABLE_DIALOG)) +#define E_IS_HTML_EDITOR_TABLE_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_TABLE_DIALOG)) +#define E_HTML_EDITOR_TABLE_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_TABLE_DIALOG, EHTMLEditorTableDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorTableDialog EHTMLEditorTableDialog; +typedef struct _EHTMLEditorTableDialogClass EHTMLEditorTableDialogClass; +typedef struct _EHTMLEditorTableDialogPrivate EHTMLEditorTableDialogPrivate; + +struct _EHTMLEditorTableDialog { + EHTMLEditorDialog parent; + EHTMLEditorTableDialogPrivate *priv; +}; + +struct _EHTMLEditorTableDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_table_dialog_get_type (void) G_GNUC_CONST; +GtkWidget * e_html_editor_table_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_TABLE_DIALOG_H */ diff --git a/e-util/e-html-editor-text-dialog.c b/e-util/e-html-editor-text-dialog.c new file mode 100644 index 0000000000..2db792c70a --- /dev/null +++ b/e-util/e-html-editor-text-dialog.c @@ -0,0 +1,298 @@ +/* + * e-html-editor-text-dialog.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-text-dialog.h" + +#include <glib/gi18n-lib.h> + +#include "e-color-combo.h" + +#define E_HTML_EDITOR_TEXT_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_TEXT_DIALOG, EHTMLEditorTextDialogPrivate)) + +struct _EHTMLEditorTextDialogPrivate { + GtkWidget *bold_check; + GtkWidget *italic_check; + GtkWidget *underline_check; + GtkWidget *strikethrough_check; + + GtkWidget *color_check; + GtkWidget *size_check; +}; + +G_DEFINE_TYPE ( + EHTMLEditorTextDialog, + e_html_editor_text_dialog, + E_TYPE_HTML_EDITOR_DIALOG); + +static void +html_editor_text_dialog_set_bold (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_set_bold ( + selection, + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->bold_check))); +} + +static void +html_editor_text_dialog_set_italic (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_set_italic ( + selection, + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->italic_check))); +} + +static void +html_editor_text_dialog_set_underline (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_set_underline ( + selection, + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->underline_check))); +} + +static void +html_editor_text_dialog_set_strikethrough (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_html_editor_selection_set_strikethrough ( + selection, + gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (dialog->priv->strikethrough_check))); +} + +static void +html_editor_text_dialog_set_color (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + GdkRGBA rgba; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + e_color_combo_get_current_color ( + E_COLOR_COMBO (dialog->priv->color_check), &rgba); + e_html_editor_selection_set_font_color (selection, &rgba); +} + +static void +html_editor_text_dialog_set_size (EHTMLEditorTextDialog *dialog) +{ + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + gint size; + + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + size = gtk_combo_box_get_active (GTK_COMBO_BOX (dialog->priv->size_check)); + + e_html_editor_selection_set_font_size (selection, size + 1); +} + +static void +html_editor_text_dialog_show (GtkWidget *widget) +{ + EHTMLEditorTextDialog *dialog; + EHTMLEditor *editor; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + GdkRGBA rgba; + + dialog = E_HTML_EDITOR_TEXT_DIALOG (widget); + editor = e_html_editor_dialog_get_editor (E_HTML_EDITOR_DIALOG (dialog)); + view = e_html_editor_get_view (editor); + selection = e_html_editor_view_get_selection (view); + + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->bold_check), + e_html_editor_selection_is_bold (selection)); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->italic_check), + e_html_editor_selection_is_italic (selection)); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->underline_check), + e_html_editor_selection_is_underline (selection)); + gtk_toggle_button_set_active ( + GTK_TOGGLE_BUTTON (dialog->priv->strikethrough_check), + e_html_editor_selection_is_strikethrough (selection)); + + gtk_combo_box_set_active ( + GTK_COMBO_BOX (dialog->priv->size_check), + e_html_editor_selection_get_font_size (selection)); + + e_html_editor_selection_get_font_color (selection, &rgba); + e_color_combo_set_current_color ( + E_COLOR_COMBO (dialog->priv->color_check), &rgba); + + GTK_WIDGET_CLASS (e_html_editor_text_dialog_parent_class)->show (widget); +} + +static void +e_html_editor_text_dialog_class_init (EHTMLEditorTextDialogClass *class) +{ + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorTextDialogPrivate)); + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = html_editor_text_dialog_show; +} + +static void +e_html_editor_text_dialog_init (EHTMLEditorTextDialog *dialog) +{ + GtkGrid *main_layout; + GtkWidget *widget; + + dialog->priv = E_HTML_EDITOR_TEXT_DIALOG_GET_PRIVATE (dialog); + + main_layout = e_html_editor_dialog_get_container (E_HTML_EDITOR_DIALOG (dialog)); + + /* Bold */ + widget = gtk_image_new_from_stock (GTK_STOCK_BOLD, GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (main_layout, widget, 0, 0, 1, 1); + + widget = gtk_check_button_new_with_mnemonic (_("_Bold")); + gtk_grid_attach (main_layout, widget, 1, 0, 1, 1); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_text_dialog_set_bold), dialog); + dialog->priv->bold_check = widget; + + /* Italic */ + widget = gtk_image_new_from_stock (GTK_STOCK_ITALIC, GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (main_layout, widget, 0, 1, 1, 1); + + widget = gtk_check_button_new_with_mnemonic (_("_Italic")); + gtk_grid_attach (main_layout, widget, 1, 1, 1, 1); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_text_dialog_set_italic), dialog); + dialog->priv->italic_check = widget; + + /* Underline */ + widget = gtk_image_new_from_stock (GTK_STOCK_UNDERLINE, GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (main_layout, widget, 0, 2, 1, 1); + + widget = gtk_check_button_new_with_mnemonic (_("_Underline")); + gtk_grid_attach (main_layout, widget, 1, 2, 1, 1); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_text_dialog_set_underline), dialog); + dialog->priv->underline_check = widget; + + widget = gtk_image_new_from_stock (GTK_STOCK_STRIKETHROUGH, GTK_ICON_SIZE_BUTTON); + gtk_grid_attach (main_layout, widget, 0, 3, 1, 1); + + widget = gtk_check_button_new_with_mnemonic (_("_Strikethrough")); + gtk_grid_attach (main_layout, widget, 1, 3, 1, 1); + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (html_editor_text_dialog_set_strikethrough), dialog); + dialog->priv->strikethrough_check = widget; + + /* Color */ + widget = e_color_combo_new (); + gtk_grid_attach (main_layout, widget, 3, 0, 1, 1); + g_signal_connect_swapped ( + widget, "notify::current-color", + G_CALLBACK (html_editor_text_dialog_set_color), dialog); + dialog->priv->color_check = widget; + + widget = gtk_label_new_with_mnemonic (_("_Color:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->color_check); + gtk_grid_attach (main_layout, widget, 2, 0, 1, 1); + + /* Size */ + widget = gtk_combo_box_text_new (); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "minus-two", "-2"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "minus-one", "-1"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "minus-zero", "0"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "plus-one", "+1"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "plus-two", "+2"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "plus-three", "+3"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "plus-four", "+4"); + gtk_grid_attach (main_layout, widget, 3, 1, 1, 1); + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (html_editor_text_dialog_set_size), dialog); + dialog->priv->size_check = widget; + + widget = gtk_label_new_with_mnemonic (_("Si_ze:")); + gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_RIGHT); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->size_check); + gtk_grid_attach (main_layout, widget, 2, 1, 1, 1); + + gtk_widget_show_all (GTK_WIDGET (main_layout)); +} + +GtkWidget * +e_html_editor_text_dialog_new (EHTMLEditor *editor) +{ + return GTK_WIDGET ( + g_object_new ( + E_TYPE_HTML_EDITOR_TEXT_DIALOG, + "editor", editor, + "title", N_("Text Properties"), + NULL)); +} diff --git a/e-util/e-html-editor-text-dialog.h b/e-util/e-html-editor-text-dialog.h new file mode 100644 index 0000000000..006b780009 --- /dev/null +++ b/e-util/e-html-editor-text-dialog.h @@ -0,0 +1,69 @@ +/* + * e-html-editor-text-dialog.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_TEXT_DIALOG_H +#define E_HTML_EDITOR_TEXT_DIALOG_H + +#include <e-util/e-html-editor-dialog.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_TEXT_DIALOG \ + (e_html_editor_text_dialog_get_type ()) +#define E_HTML_EDITOR_TEXT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_TEXT_DIALOG, EHTMLEditorTextDialog)) +#define E_HTML_EDITOR_TEXT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_TEXT_DIALOG, EHTMLEditorTextDialogClass)) +#define E_IS_HTML_EDITOR_TEXT_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_TEXT_DIALOG)) +#define E_IS_HTML_EDITOR_TEXT_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_TEXT_DIALOG)) +#define E_HTML_EDITOR_TEXT_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_TEXT_DIALOG, EHTMLEditorTextDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorTextDialog EHTMLEditorTextDialog; +typedef struct _EHTMLEditorTextDialogClass EHTMLEditorTextDialogClass; +typedef struct _EHTMLEditorTextDialogPrivate EHTMLEditorTextDialogPrivate; + +struct _EHTMLEditorTextDialog { + EHTMLEditorDialog parent; + EHTMLEditorTextDialogPrivate *priv; +}; + +struct _EHTMLEditorTextDialogClass { + EHTMLEditorDialogClass parent_class; +}; + +GType e_html_editor_text_dialog_get_type (void) G_GNUC_CONST; +GtkWidget * e_html_editor_text_dialog_new (EHTMLEditor *editor); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_TEXT_DIALOG_H */ diff --git a/e-util/e-html-editor-utils.c b/e-util/e-html-editor-utils.c new file mode 100644 index 0000000000..2807ea94b0 --- /dev/null +++ b/e-util/e-html-editor-utils.c @@ -0,0 +1,116 @@ +/* + * e-html-editor-utils.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-utils.h" +#include <string.h> + +/** + * e_html_editor_dom_node_find_parent_element: + * @node: Start node + * @tagname: Tag name of element to search + * + * Recursively searches for first occurance of element with given @tagname + * that is parent of given @node. + * + * Returns: A #WebKitDOMElement with @tagname representing parent of @node or + * @NULL when @node has no parent with given @tagname. When @node matches @tagname, + * then the @node is returned. + */ +WebKitDOMElement * +e_html_editor_dom_node_find_parent_element (WebKitDOMNode *node, + const gchar *tagname) +{ + gint taglen = strlen (tagname); + + while (node) { + + if (WEBKIT_DOM_IS_ELEMENT (node)) { + gchar *node_tagname; + + node_tagname = webkit_dom_element_get_tag_name ( + WEBKIT_DOM_ELEMENT (node)); + + if (node_tagname && + (strlen (node_tagname) == taglen) && + (g_ascii_strncasecmp (node_tagname, tagname, taglen) == 0)) { + g_free (node_tagname); + return WEBKIT_DOM_ELEMENT (node); + } + + g_free (node_tagname); + } + + node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node)); + } + + return NULL; +} + +/** + * e_html_editor_dom_node_find_child_element: + * @node: Start node + * @tagname: Tag name of element to search. + * + * Recursively searches for first occurence of element with given @tagname that + * is a child of @node. + * + * Returns: A #WebKitDOMElement with @tagname representing a child of @node or + * @NULL when @node has no child with given @tagname. When @node matches @tagname, + * then the @node is returned. + */ +WebKitDOMElement * +e_html_editor_dom_node_find_child_element (WebKitDOMNode *node, + const gchar *tagname) +{ + WebKitDOMNode *start_node = node; + gint taglen = strlen (tagname); + + do { + if (WEBKIT_DOM_IS_ELEMENT (node)) { + gchar *node_tagname; + + node_tagname = webkit_dom_element_get_tag_name ( + WEBKIT_DOM_ELEMENT (node)); + + if (node_tagname && + (strlen (node_tagname) == taglen) && + (g_ascii_strncasecmp (node_tagname, tagname, taglen) == 0)) { + g_free (node_tagname); + return WEBKIT_DOM_ELEMENT (node); + } + + g_free (node_tagname); + } + + if (webkit_dom_node_has_child_nodes (node)) { + node = webkit_dom_node_get_first_child (node); + } else if (webkit_dom_node_get_next_sibling (node)) { + node = webkit_dom_node_get_next_sibling (node); + } else { + node = webkit_dom_node_get_parent_node (node); + } + } while (!webkit_dom_node_is_same_node (node, start_node)); + + return NULL; +} diff --git a/e-util/e-html-editor-utils.h b/e-util/e-html-editor-utils.h new file mode 100644 index 0000000000..7331a87709 --- /dev/null +++ b/e-util/e-html-editor-utils.h @@ -0,0 +1,44 @@ +/* + * e-html-editor-utils.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_UTILS_H +#define E_HTML_EDITOR_UTILS_H + +#include <webkit/webkitdom.h> + +G_BEGIN_DECLS + +WebKitDOMElement * + e_html_editor_dom_node_find_parent_element + (WebKitDOMNode *node, + const gchar *tagname); + +WebKitDOMElement * + e_html_editor_dom_node_find_child_element + (WebKitDOMNode *node, + const gchar *tagname); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_UTILS_H */ diff --git a/e-util/e-html-editor-view.c b/e-util/e-html-editor-view.c new file mode 100644 index 0000000000..21cadb3a5e --- /dev/null +++ b/e-util/e-html-editor-view.c @@ -0,0 +1,6303 @@ +/* + * e-html-editor-view.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-view.h" +#include "e-html-editor.h" +#include "e-emoticon-chooser.h" + +#include <e-util/e-util.h> +#include <e-util/e-marshal.h> +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> + +#define E_HTML_EDITOR_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_VIEW, EHTMLEditorViewPrivate)) + +#define UNICODE_ZERO_WIDTH_SPACE "\xe2\x80\x8b" + +#define URL_PATTERN \ + "((([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[\\-;:&=\\+\\$,\\w]+@)?" \ + "[A-Za-z0-9\\.\\-]+|(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)" \ + "[A-Za-z0-9\\.\\-]+)((?:\\/[\\+~%\\/\\.\\w\\-]*)?\\?" \ + "?(?:[\\-\\+=&;%@\\.\\w]*)#?(?:[\\.\\!\\/\\\\w]*))?)" + +#define URL_PATTERN_SPACE URL_PATTERN "\\s" + +#define QUOTE_SYMBOL ">" + +/* Keep synchronized with the same value in EHTMLEditorSelection */ +#define SPACES_PER_LIST_LEVEL 8 + +/** + * EHTMLEditorView: + * + * The #EHTMLEditorView is a WebKit-based rich text editor. The view itself + * only provides means to configure global behavior of the editor. To work + * with the actual content, current cursor position or current selection, + * use #EHTMLEditorSelection object. + */ + +struct _EHTMLEditorViewPrivate { + gint changed : 1; + gint inline_spelling : 1; + gint magic_links : 1; + gint magic_smileys : 1; + gint can_copy : 1; + gint can_cut : 1; + gint can_paste : 1; + gint can_redo : 1; + gint can_undo : 1; + gint reload_in_progress : 1; + gint html_mode : 1; + + EHTMLEditorSelection *selection; + + WebKitDOMElement *element_under_mouse; + + GHashTable *inline_images; + + GSettings *font_settings; + GSettings *aliasing_settings; + + gboolean convertor_insert; + + WebKitWebView *convertor_web_view; +}; + +enum { + PROP_0, + PROP_CAN_COPY, + PROP_CAN_CUT, + PROP_CAN_PASTE, + PROP_CAN_REDO, + PROP_CAN_UNDO, + PROP_CHANGED, + PROP_HTML_MODE, + PROP_INLINE_SPELLING, + PROP_MAGIC_LINKS, + PROP_MAGIC_SMILEYS, + PROP_SPELL_CHECKER +}; + +enum { + POPUP_EVENT, + PASTE_PRIMARY_CLIPBOARD, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static CamelDataCache *emd_global_http_cache = NULL; + +G_DEFINE_TYPE_WITH_CODE ( + EHTMLEditorView, + e_html_editor_view, + WEBKIT_TYPE_WEB_VIEW, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static WebKitDOMRange * +html_editor_view_get_dom_range (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *selection; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + selection = webkit_dom_dom_window_get_selection (window); + + if (webkit_dom_dom_selection_get_range_count (selection) < 1) { + return NULL; + } + + return webkit_dom_dom_selection_get_range_at (selection, 0, NULL); +} + +static void +html_editor_view_user_changed_contents_cb (EHTMLEditorView *view, + gpointer user_data) +{ + WebKitWebView *web_view; + gboolean can_redo, can_undo; + + web_view = WEBKIT_WEB_VIEW (view); + + e_html_editor_view_set_changed (view, TRUE); + + can_redo = webkit_web_view_can_redo (web_view); + if (view->priv->can_redo != can_redo) { + view->priv->can_redo = can_redo; + g_object_notify (G_OBJECT (view), "can-redo"); + } + + can_undo = webkit_web_view_can_undo (web_view); + if (view->priv->can_undo != can_undo) { + view->priv->can_undo = can_undo; + g_object_notify (G_OBJECT (view), "can-undo"); + } +} + +static void +html_editor_view_selection_changed_cb (EHTMLEditorView *view, + gpointer user_data) +{ + WebKitWebView *web_view; + gboolean can_copy, can_cut, can_paste; + + web_view = WEBKIT_WEB_VIEW (view); + + /* When the webview is being (re)loaded, the document is in an + * inconsistant state and there is no selection, so don't propagate + * the signal further to EHTMLEditorSelection and others and wait until + * the load is finished. */ + if (view->priv->reload_in_progress) { + g_signal_stop_emission_by_name (view, "selection-changed"); + return; + } + + can_copy = webkit_web_view_can_copy_clipboard (web_view); + if (view->priv->can_copy != can_copy) { + view->priv->can_copy = can_copy; + g_object_notify (G_OBJECT (view), "can-copy"); + } + + can_cut = webkit_web_view_can_cut_clipboard (web_view); + if (view->priv->can_cut != can_cut) { + view->priv->can_cut = can_cut; + g_object_notify (G_OBJECT (view), "can-cut"); + } + + can_paste = webkit_web_view_can_paste_clipboard (web_view); + if (view->priv->can_paste != can_paste) { + view->priv->can_paste = can_paste; + g_object_notify (G_OBJECT (view), "can-paste"); + } +} + +static gboolean +html_editor_view_should_show_delete_interface_for_element (EHTMLEditorView *view, + WebKitDOMHTMLElement *element) +{ + return FALSE; +} + +void +e_html_editor_view_force_spell_check_for_current_paragraph (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + WebKitDOMDocument *document; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMDOMWindow *window; + WebKitDOMElement *caret, *parent, *element; + WebKitDOMRange *end_range, *actual; + WebKitDOMText *text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + element = webkit_dom_document_query_selector ( + document, "body[spellcheck=true]", NULL); + + if (!element) + return; + + selection = e_html_editor_view_get_selection (view); + caret = e_html_editor_selection_save_caret_position (selection); + + /* Block callbacks of selection-changed signal as we don't want to + * recount all the block format things in EHTMLEditorSelection and here as well + * when we are moving with caret */ + g_signal_handlers_block_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_block_selection_changed (selection); + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (caret)); + element = caret; + + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + element = parent; + parent = webkit_dom_node_get_parent_element ( + WEBKIT_DOM_NODE (parent)); + } + + /* Append some text on the end of the element */ + text = webkit_dom_document_create_text_node (document, "-x-evo-end"); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (text), NULL); + + /* Create range that's pointing on the end of this text */ + end_range = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + end_range, WEBKIT_DOM_NODE (text), NULL); + webkit_dom_range_collapse (end_range, FALSE, NULL); + + /* Move on the beginning of the paragraph */ + actual = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + actual, WEBKIT_DOM_NODE (element), NULL); + webkit_dom_range_collapse (actual, TRUE, NULL); + webkit_dom_dom_selection_remove_all_ranges (dom_selection); + webkit_dom_dom_selection_add_range (dom_selection, actual); + + /* Go through all words to spellcheck them. To avoid this we have to wait for + * http://www.w3.org/html/wg/drafts/html/master/editing.html#dom-forcespellcheck */ + actual = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + /* We are moving forward word by word until we hit the text on the end of + * the paragraph that we previously inserted there */ + while (actual && webkit_dom_range_compare_boundary_points (end_range, 2, actual, NULL) != 0) { + webkit_dom_dom_selection_modify ( + dom_selection, "move", "forward", "word"); + actual = webkit_dom_dom_selection_get_range_at ( + dom_selection, 0, NULL); + } + + /* Remove the text that we inserted on the end of the paragraph */ + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (text), NULL); + + /* Unblock the callbacks */ + g_signal_handlers_unblock_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_unblock_selection_changed (selection); + + e_html_editor_selection_restore_caret_position (selection); +} + +static void +move_caret_into_element (WebKitDOMDocument *document, + WebKitDOMElement *element) +{ + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *window_selection; + WebKitDOMRange *new_range; + + if (!element) + return; + + window = webkit_dom_document_get_default_view (document); + window_selection = webkit_dom_dom_window_get_selection (window); + new_range = webkit_dom_document_create_range (document); + + webkit_dom_range_select_node_contents ( + new_range, WEBKIT_DOM_NODE (element), NULL); + webkit_dom_range_collapse (new_range, FALSE, NULL); + webkit_dom_dom_selection_remove_all_ranges (window_selection); + webkit_dom_dom_selection_add_range (window_selection, new_range); +} + +static void +refresh_spell_check (EHTMLEditorView *view, + gboolean enable_spell_check) +{ + EHTMLEditorSelection *selection; + WebKitDOMDocument *document; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMDOMWindow *window; + WebKitDOMHTMLElement *body; + WebKitDOMRange *end_range, *actual; + WebKitDOMText *text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + /* Enable/Disable spellcheck in composer */ + body = webkit_dom_document_get_body (document); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), + "spellcheck", + enable_spell_check ? "true" : "false", + NULL); + + selection = e_html_editor_view_get_selection (view); + e_html_editor_selection_save_caret_position (selection); + + /* Sometimes the web view is not event focused, so we have to move caret + * into body */ + if (!webkit_dom_document_get_element_by_id (document, "-x-evo-caret-position")) { + move_caret_into_element ( + document, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document))); + e_html_editor_selection_save_caret_position (selection); + } + + /* Block callbacks of selection-changed signal as we don't want to + * recount all the block format things in EHTMLEditorSelection and here as well + * when we are moving with caret */ + g_signal_handlers_block_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_block_selection_changed (selection); + + /* Append some text on the end of the body */ + text = webkit_dom_document_create_text_node (document, "-x-evo-end"); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (text), NULL); + + /* Create range that's pointing on the end of this text */ + end_range = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + end_range, WEBKIT_DOM_NODE (text), NULL); + webkit_dom_range_collapse (end_range, FALSE, NULL); + + /* Move on the beginning of the document */ + webkit_dom_dom_selection_modify ( + dom_selection, "move", "backward", "documentboundary"); + + /* Go through all words to spellcheck them. To avoid this we have to wait for + * http://www.w3.org/html/wg/drafts/html/master/editing.html#dom-forcespellcheck */ + actual = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + /* We are moving forward word by word until we hit the text on the end of + * the body that we previously inserted there */ + while (actual && webkit_dom_range_compare_boundary_points (end_range, 2, actual, NULL) != 0) { + webkit_dom_dom_selection_modify ( + dom_selection, "move", "forward", "word"); + actual = webkit_dom_dom_selection_get_range_at ( + dom_selection, 0, NULL); + } + + /* Remove the text that we inserted on the end of the body */ + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (text), NULL); + + /* Unblock the callbacks */ + g_signal_handlers_unblock_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_unblock_selection_changed (selection); + + e_html_editor_selection_restore_caret_position (selection); +} + +void +e_html_editor_view_turn_spell_check_off (EHTMLEditorView *view) +{ + refresh_spell_check (view, FALSE); +} + +void +e_html_editor_view_force_spell_check (EHTMLEditorView *view) +{ + refresh_spell_check (view, TRUE); +} + +static void +body_input_event_cb (WebKitDOMElement *element, + WebKitDOMEvent *event, + EHTMLEditorView *view) +{ + WebKitDOMNode *node; + WebKitDOMRange *range = html_editor_view_get_dom_range (view); + + e_html_editor_view_set_changed (view, TRUE); + + node = webkit_dom_range_get_end_container (range, NULL); + + /* After toggling monospaced format, we are using UNICODE_ZERO_WIDTH_SPACE + * to move caret into right space. When this callback is called it is not + * necassary anymore so remove it */ + if (e_html_editor_view_get_html_mode (view)) { + WebKitDOMElement *parent = webkit_dom_node_get_parent_element (node); + + if (parent) { + WebKitDOMNode *prev_sibling; + + prev_sibling = webkit_dom_node_get_previous_sibling ( + WEBKIT_DOM_NODE (parent)); + + if (prev_sibling && WEBKIT_DOM_IS_TEXT (prev_sibling)) { + gchar *text = webkit_dom_node_get_text_content ( + prev_sibling); + + if (g_strcmp0 (text, UNICODE_ZERO_WIDTH_SPACE) == 0) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node ( + prev_sibling), + prev_sibling, + NULL); + } + g_free (text); + } + + } + } + + /* If text before caret includes UNICODE_ZERO_WIDTH_SPACE character, remove it */ + if (WEBKIT_DOM_IS_TEXT (node)) { + gchar *text = webkit_dom_character_data_get_data (WEBKIT_DOM_CHARACTER_DATA (node)); + glong length = g_utf8_strlen (text, -1); + WebKitDOMNode *parent; + + /* We have to preserve empty paragraphs with just UNICODE_ZERO_WIDTH_SPACE + * character as when we will remove it it will collapse */ + if (length > 1) { + if (g_str_has_prefix (text, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (node), 0, 1, "", NULL); + else if (g_str_has_suffix (text, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (node), length - 1, 1, "", NULL); + } + g_free (text); + + parent = webkit_dom_node_get_parent_node (node); + if ((WEBKIT_DOM_IS_HTML_PARAGRAPH_ELEMENT (parent) || + WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent)) && + !element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-paragraph")) { + if (e_html_editor_view_get_html_mode (view)) { + element_add_class ( + WEBKIT_DOM_ELEMENT (parent), "-x-evo-paragraph"); + } else { + e_html_editor_selection_set_paragraph_style ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (parent), + -1, 0, ""); + } + } + + /* When new smiley is added we have to use UNICODE_HIDDEN_SPACE to set the + * caret position to right place. It is removed when user starts typing. But + * when the user will press left arrow he will move the caret into + * smiley wrapper. If he will start to write there we have to move the written + * text out of the wrapper and move caret to right place */ + if (WEBKIT_DOM_IS_ELEMENT (parent) && + element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-smiley-wrapper")) { + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (parent), + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_next_sibling (parent), + NULL); + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (parent), + node, + webkit_dom_node_get_next_sibling (parent), + NULL); + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + } + } +} + +static void +set_base64_to_element_attribute (EHTMLEditorView *view, + WebKitDOMElement *element, + const gchar *attribute) +{ + gchar *attribute_value; + const gchar *base64_src; + + attribute_value = webkit_dom_element_get_attribute (element, attribute); + + if ((base64_src = g_hash_table_lookup (view->priv->inline_images, attribute_value)) != NULL) { + const gchar *base64_data = strstr (base64_src, ";") + 1; + gchar *name; + glong name_length; + + name_length = + g_utf8_strlen (base64_src, -1) - + g_utf8_strlen (base64_data, -1) - 1; + name = g_strndup (base64_src, name_length); + + webkit_dom_element_set_attribute (element, "data-inline", "", NULL); + webkit_dom_element_set_attribute (element, "data-name", name, NULL); + webkit_dom_element_set_attribute (element, attribute, base64_data, NULL); + + g_free (name); + } +} + +static void +change_cid_images_src_to_base64 (EHTMLEditorView *view) +{ + gint ii, length; + WebKitDOMDocument *document; + WebKitDOMElement *document_element; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + document_element = webkit_dom_document_get_document_element (document); + + list = webkit_dom_document_query_selector_all (document, "img[src^=\"cid:\"]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + set_base64_to_element_attribute (view, WEBKIT_DOM_ELEMENT (node), "src"); + } + + /* Namespaces */ + attributes = webkit_dom_element_get_attributes (document_element); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = 0; ii < length; ii++) { + gchar *name; + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + name = webkit_dom_node_get_local_name (node); + + if (g_str_has_prefix (name, "xmlns:")) { + const gchar *ns = name + 6; + gchar *attribute_ns = g_strconcat (ns, ":src", NULL); + gchar *selector = g_strconcat ("img[", ns, "\\:src^=\"cid:\"]", NULL); + gint ns_length, jj; + + list = webkit_dom_document_query_selector_all ( + document, selector, NULL); + ns_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < ns_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + set_base64_to_element_attribute ( + view, WEBKIT_DOM_ELEMENT (node), attribute_ns); + } + + g_free (attribute_ns); + g_free (selector); + } + g_free (name); + } + + list = webkit_dom_document_query_selector_all ( + document, "[background^=\"cid:\"]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + set_base64_to_element_attribute ( + view, WEBKIT_DOM_ELEMENT (node), "background"); + } + g_hash_table_remove_all (view->priv->inline_images); +} + +/* For purpose of this function see e-mail-formatter-quote.c */ +static void +put_body_in_citation (WebKitDOMDocument *document) +{ + WebKitDOMElement *cite_body = webkit_dom_document_query_selector ( + document, "span.-x-evo-cite-body", NULL); + + if (cite_body) { + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + gchar *inner_html, *with_citation; + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (cite_body), + NULL); + + inner_html = webkit_dom_html_element_get_inner_html (body); + with_citation = g_strconcat ( + "<blockquote type=\"cite\" id=\"-x-evo-main-cite\">", + inner_html, "</span>", NULL); + webkit_dom_html_element_set_inner_html (body, with_citation, NULL); + g_free (inner_html); + g_free (with_citation); + } +} + +/* For purpose of this function see e-mail-formatter-quote.c */ +static void +move_elements_to_body (WebKitDOMDocument *document) +{ + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + WebKitDOMNodeList *list; + gint ii; + + list = webkit_dom_document_query_selector_all ( + document, "span.-x-evo-to-body", NULL); + for (ii = webkit_dom_node_list_get_length (list) - 1; ii >= 0; ii--) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + while (webkit_dom_node_has_child_nodes (node)) { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + webkit_dom_node_get_first_child (node), + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)), + NULL); + } + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (node), + NULL); + } +} + +static void +repair_gmail_blockquotes (WebKitDOMDocument *document) +{ + WebKitDOMNodeList *list; + gint ii, length; + + list = webkit_dom_document_query_selector_all ( + document, "blockquote.gmail_quote", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (node), "class"); + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (node), "style"); + webkit_dom_element_set_attribute (WEBKIT_DOM_ELEMENT (node), "type", "cite", NULL); + } +} + +static void +html_editor_view_load_status_changed (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitLoadStatus status; + + status = webkit_web_view_get_load_status (WEBKIT_WEB_VIEW (view)); + if (status != WEBKIT_LOAD_FINISHED) + return; + + view->priv->reload_in_progress = FALSE; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (body), "style"); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-message", "", NULL); + + put_body_in_citation (document); + move_elements_to_body (document); + repair_gmail_blockquotes (document); + + /* Register on input event that is called when the content (body) is modified */ + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (body), + "input", + G_CALLBACK (body_input_event_cb), + FALSE, + view); + + if (view->priv->html_mode) + change_cid_images_src_to_base64 (view); +} + +/* Based on original use_pictograms() from GtkHTML */ +static const gchar *emoticons_chars = + /* 0 */ "DO)(|/PQ*!" + /* 10 */ "S\0:-\0:\0:-\0" + /* 20 */ ":\0:;=-\"\0:;" + /* 30 */ "B\"|\0:-'\0:X" + /* 40 */ "\0:\0:-\0:\0:-" + /* 50 */ "\0:\0:-\0:\0:-" + /* 60 */ "\0:\0:\0:-\0:\0" + /* 70 */ ":-\0:\0:-\0:\0"; +static gint emoticons_states[] = { + /* 0 */ 12, 17, 22, 34, 43, 48, 53, 58, 65, 70, + /* 10 */ 75, 0, -15, 15, 0, -15, 0, -17, 20, 0, + /* 20 */ -17, 0, -14, -20, -14, 28, 63, 0, -14, -20, + /* 30 */ -3, 63, -18, 0, -12, 38, 41, 0, -12, -2, + /* 40 */ 0, -4, 0, -10, 46, 0, -10, 0, -19, 51, + /* 50 */ 0, -19, 0, -11, 56, 0, -11, 0, -13, 61, + /* 60 */ 0, -13, 0, -6, 0, 68, -7, 0, -7, 0, + /* 70 */ -16, 73, 0, -16, 0, -21, 78, 0, -21, 0 }; +static const gchar *emoticons_icon_names[] = { + "face-angel", + "face-angry", + "face-cool", + "face-crying", + "face-devilish", + "face-embarrassed", + "face-kiss", + "face-laugh", /* not used */ + "face-monkey", /* not used */ + "face-plain", + "face-raspberry", + "face-sad", + "face-sick", + "face-smile", + "face-smile-big", + "face-smirk", + "face-surprise", + "face-tired", + "face-uncertain", + "face-wink", + "face-worried" +}; + +static void +html_editor_view_check_magic_links (EHTMLEditorView *view, + WebKitDOMRange *range, + gboolean include_space_by_user, + GdkEventKey *event) +{ + gchar *node_text; + gchar **urls; + GRegex *regex = NULL; + GMatchInfo *match_info; + gint start_pos_url, end_pos_url; + WebKitDOMNode *node; + gboolean include_space = FALSE; + gboolean return_pressed = FALSE; + + if (event != NULL) { + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_Linefeed) || + (event->keyval == GDK_KEY_KP_Enter)) { + + return_pressed = TRUE; + } + + if (event->keyval == GDK_KEY_space) + include_space = TRUE; + } else { + include_space = include_space_by_user; + } + + node = webkit_dom_range_get_end_container (range, NULL); + + if (return_pressed) + node = webkit_dom_node_get_previous_sibling (node); + + if (!node) + return; + + if (!WEBKIT_DOM_IS_TEXT (node)) { + if (webkit_dom_node_has_child_nodes (node)) + node = webkit_dom_node_get_first_child (node); + if (!WEBKIT_DOM_IS_TEXT (node)) + return; + } + + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + if (!node_text || !(*node_text) || !g_utf8_validate (node_text, -1, NULL)) + return; + + regex = g_regex_new (include_space ? URL_PATTERN_SPACE : URL_PATTERN, 0, 0, NULL); + + if (!regex) { + g_free (node_text); + return; + } + + g_regex_match_all (regex, node_text, G_REGEX_MATCH_NOTEMPTY, &match_info); + urls = g_match_info_fetch_all (match_info); + + if (urls) { + gchar *final_url, *url_end_raw; + glong url_start, url_end, url_length; + WebKitDOMDocument *document; + WebKitDOMNode *url_text_node_clone; + WebKitDOMText *url_text_node; + WebKitDOMElement *anchor; + const gchar* url_text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + if (!return_pressed) + e_html_editor_selection_save_caret_position ( + e_html_editor_view_get_selection (view)); + + g_match_info_fetch_pos (match_info, 0, &start_pos_url, &end_pos_url); + + /* Get start and end position of url in node's text because positions + * that we get from g_match_info_fetch_pos are not UTF-8 aware */ + url_end_raw = g_strndup(node_text, end_pos_url); + url_end = g_utf8_strlen (url_end_raw, -1); + + url_length = g_utf8_strlen (urls[0], -1); + url_start = url_end - url_length; + + webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), + include_space ? url_end - 1 : url_end, + NULL); + + url_text_node = webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), url_start, NULL); + url_text_node_clone = webkit_dom_node_clone_node ( + WEBKIT_DOM_NODE (url_text_node), TRUE); + url_text = webkit_dom_text_get_whole_text ( + WEBKIT_DOM_TEXT (url_text_node_clone)); + + final_url = g_strconcat ( + g_str_has_prefix (url_text, "www") ? "http://" : "", url_text, NULL); + + /* Create and prepare new anchor element */ + anchor = webkit_dom_document_create_element (document, "A", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (anchor), + url_text, + NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (anchor), + final_url); + + /* Insert new anchor element into document */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (anchor), + WEBKIT_DOM_NODE (url_text_node), + NULL); + + if (!return_pressed) + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + + g_free (url_end_raw); + g_free (final_url); + } else { + WebKitDOMElement *parent; + WebKitDOMNode *prev_sibling; + gchar *href, *text, *url; + gint diff; + const char* text_to_append; + gboolean appending_to_link = FALSE; + + parent = webkit_dom_node_get_parent_element (node); + prev_sibling = webkit_dom_node_get_previous_sibling (node); + + /* If previous sibling is ANCHOR and actual text node is not beginning with + * space => we're appending to link */ + if (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling)) { + text_to_append = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_to_append, "") != 0 && + !g_unichar_isspace (g_utf8_get_char (text_to_append))) { + + appending_to_link = TRUE; + parent = WEBKIT_DOM_ELEMENT (prev_sibling); + } + } + + /* If parent is ANCHOR => we're editing the link */ + if (!WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (parent) && !appending_to_link) { + g_match_info_free (match_info); + g_regex_unref (regex); + g_free (node_text); + return; + } + + /* edit only if href and description are the same */ + href = webkit_dom_html_anchor_element_get_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent)); + + if (appending_to_link) { + gchar *inner_text; + + inner_text = + webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (parent)), + + text = g_strconcat (inner_text, text_to_append, NULL); + g_free (inner_text); + } else + text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + + if (strstr (href, "://") && !strstr (text, "://")) { + url = strstr (href, "://") + 3; + diff = strlen (text) - strlen (url); + + if (text [strlen (text) - 1] != '/') + diff++; + + if ((g_strcmp0 (url, text) != 0 && ABS (diff) == 1) || appending_to_link) { + gchar *inner_html, *protocol, *new_href; + + protocol = g_strndup (href, strstr (href, "://") - href + 3); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + new_href = g_strconcat ( + protocol, inner_html, appending_to_link ? text_to_append : "", NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent), + new_href); + + if (appending_to_link) { + gchar *tmp; + + tmp = g_strconcat (inner_html, text_to_append, NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent), + tmp, + NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, NULL); + + g_free (tmp); + } + + g_free (new_href); + g_free (protocol); + g_free (inner_html); + } + } else { + diff = strlen (text) - strlen (href); + if (text [strlen (text) - 1] != '/') + diff++; + + if ((g_strcmp0 (href, text) != 0 && ABS (diff) == 1) || appending_to_link) { + gchar *inner_html; + gchar *new_href; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + new_href = g_strconcat ( + inner_html, + appending_to_link ? text_to_append : "", + NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent), + new_href); + + if (appending_to_link) { + gchar *tmp; + + tmp = g_strconcat (inner_html, text_to_append, NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent), + tmp, + NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, NULL); + + g_free (tmp); + } + + g_free (new_href); + g_free (inner_html); + } + + } + g_free (text); + g_free (href); + } + + g_match_info_free (match_info); + g_regex_unref (regex); + g_free (node_text); +} + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + EHTMLEditorView *view; + gchar *content_type; + gchar *name; + EEmoticon *emoticon; +}; + +static LoadContext * +emoticon_load_context_new (EHTMLEditorView *view, + EEmoticon *emoticon) +{ + LoadContext *load_context; + + load_context = g_slice_new0 (LoadContext); + load_context->view = view; + load_context->emoticon = emoticon; + + return load_context; +} + +static void +emoticon_load_context_free (LoadContext *load_context) +{ + g_free (load_context->content_type); + g_free (load_context->name); + g_slice_free (LoadContext, load_context); +} + +static void +emoticon_read_async_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + EHTMLEditorView *view = load_context->view; + EEmoticon *emoticon = load_context->emoticon; + GError *error = NULL; + gchar *html, *node_text = NULL, *mime_type; + gchar *base64_encoded, *output, *data; + const gchar *emoticon_start; + GFileInputStream *input_stream; + GOutputStream *output_stream; + gssize size; + WebKitDOMDocument *document; + WebKitDOMElement *span, *caret_position; + WebKitDOMNode *node; + WebKitDOMRange *range; + + input_stream = g_file_read_finish (file, result, &error); + g_return_if_fail (!error && input_stream); + + output_stream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free); + + size = g_output_stream_splice ( + output_stream, G_INPUT_STREAM (input_stream), + G_OUTPUT_STREAM_SPLICE_NONE, NULL, &error); + + if (error || (size == -1)) + goto out; + + caret_position = e_html_editor_selection_save_caret_position ( + e_html_editor_view_get_selection (view)); + + if (caret_position) { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)); + + /* Situation when caret is restored in body and not in paragraph */ + if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + caret_position = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (caret_position), + NULL)); + + caret_position = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_insert_before ( + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (parent)), + WEBKIT_DOM_NODE (caret_position), + webkit_dom_node_get_first_child ( + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (parent))), + NULL)); + } + } + + mime_type = g_content_type_get_mime_type (load_context->content_type); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + range = html_editor_view_get_dom_range (view); + node = webkit_dom_range_get_end_container (range, NULL); + if (WEBKIT_DOM_IS_TEXT (node)) + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + span = webkit_dom_document_create_element (document, "SPAN", NULL); + + data = g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (output_stream)); + + base64_encoded = g_base64_encode ((const guchar *) data, size); + output = g_strconcat ("data:", mime_type, ";base64,", base64_encoded, NULL); + + /* Insert span with image representation and another one with text + * represetation and hide/show them dependant on active composer mode */ + /* ​ == UNICODE_ZERO_WIDTH_SPACE */ + html = g_strdup_printf ( + "<span class=\"-x-evo-smiley-wrapper -x-evo-resizable-wrapper\">" + "<img src=\"%s\" alt=\"%s\" x-evo-smiley=\"%s\" " + "class=\"-x-evo-smiley-img\" data-inline data-name=\"%s\"/>" + "<span class=\"-x-evo-smiley-text\" style=\"display: none;\">%s" + "</span></span>​", + output, emoticon ? emoticon->text_face : "", emoticon->icon_name, + load_context->name, emoticon ? emoticon->text_face : ""); + + span = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)), + WEBKIT_DOM_NODE (span), + WEBKIT_DOM_NODE (caret_position), + NULL)); + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (span), html, NULL); + + if (node_text) { + emoticon_start = g_utf8_strrchr ( + node_text, -1, g_utf8_get_char (emoticon->text_face)); + if (emoticon_start) { + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + g_utf8_strlen (node_text, -1) - strlen (emoticon_start), + strlen (emoticon->text_face), + NULL); + } + } + + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + + e_html_editor_view_set_changed (view, TRUE); + + g_free (html); + g_free (node_text); + g_free (base64_encoded); + g_free (output); + g_free (mime_type); + g_object_unref (output_stream); + out: + emoticon_load_context_free (load_context); +} + +static void +emoticon_query_info_async_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + GError *error = NULL; + GFileInfo *info; + + info = g_file_query_info_finish (file, result, &error); + g_return_if_fail (!error && info); + + load_context->content_type = g_strdup (g_file_info_get_content_type (info)); + load_context->name = g_strdup (g_file_info_get_name (info)); + + g_file_read_async ( + file, G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) emoticon_read_async_cb, load_context); + + g_object_unref (info); +} + +void +e_html_editor_view_insert_smiley (EHTMLEditorView *view, + EEmoticon *emoticon) +{ + GFile *file; + gchar *filename_uri; + LoadContext *load_context; + + filename_uri = e_emoticon_get_uri (emoticon); + g_return_if_fail (filename_uri != NULL); + + load_context = emoticon_load_context_new (view, emoticon); + + file = g_file_new_for_uri (filename_uri); + g_file_query_info_async ( + file, "standard::*", G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) emoticon_query_info_async_cb, load_context); + + g_free (filename_uri); + g_object_unref (file); +} + +static void +html_editor_view_check_magic_smileys (EHTMLEditorView *view, + WebKitDOMRange *range) +{ + gint pos; + gint state; + gint relative; + gint start; + gchar *node_text; + gunichar uc; + WebKitDOMNode *node; + + node = webkit_dom_range_get_end_container (range, NULL); + if (!WEBKIT_DOM_IS_TEXT (node)) + return; + + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + if (node_text == NULL) + return; + + start = webkit_dom_range_get_end_offset (range, NULL) - 1; + pos = start; + state = 0; + while (pos >= 0) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos)); + relative = 0; + while (emoticons_chars[state + relative]) { + if (emoticons_chars[state + relative] == uc) + break; + relative++; + } + state = emoticons_states[state + relative]; + /* 0 .. not found, -n .. found n-th */ + if (state <= 0) + break; + pos--; + } + + /* Special case needed to recognize angel and devilish. */ + if (pos > 0 && state == -14) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos - 1)); + if (uc == 'O') { + state = -1; + pos--; + } else if (uc == '>') { + state = -5; + pos--; + } + } + + if (state < 0) { + const EEmoticon *emoticon; + + if (pos > 0) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos - 1)); + if (!g_unichar_isspace (uc)) { + g_free (node_text); + return; + } + } + + emoticon = (e_emoticon_chooser_lookup_emoticon ( + emoticons_icon_names[-state - 1])); + e_html_editor_view_insert_smiley (view, (EEmoticon *) emoticon); + } + + g_free (node_text); +} + +static void +html_editor_view_set_links_active (EHTMLEditorView *view, + gboolean active) +{ + WebKitDOMDocument *document; + WebKitDOMElement *style; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + if (active) { + style = webkit_dom_document_get_element_by_id ( + document, "--evolution-editor-style-a"); + if (style) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (style)), + WEBKIT_DOM_NODE (style), NULL); + } + } else { + WebKitDOMHTMLHeadElement *head; + head = webkit_dom_document_get_head (document); + + style = webkit_dom_document_create_element (document, "STYLE", NULL); + webkit_dom_element_set_id (style, "--evolution-editor-style-a"); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (style), "a { cursor: text; }", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (head), WEBKIT_DOM_NODE (style), NULL); + } +} + +static void +clipboard_text_received (GtkClipboard *clipboard, + const gchar *text, + EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gchar *escaped_text; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMElement *blockquote, *element; + WebKitDOMNode *node; + WebKitDOMRange *range; + + if (!text || !*text) + return; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + /* This is a trick to escape any HTML characters (like <, > or &). + * <textarea> automatically replaces all these unsafe characters + * by <, > etc. */ + element = webkit_dom_document_create_element (document, "textarea", NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element), text, NULL); + escaped_text = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + element = webkit_dom_document_create_element (document, "pre", NULL); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (element), escaped_text, NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + e_html_editor_selection_get_caret_position_node (document), + NULL); + + blockquote = webkit_dom_document_create_element (document, "blockquote", NULL); + webkit_dom_element_set_attribute (blockquote, "type", "cite", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), WEBKIT_DOM_NODE (element), NULL); + + if (!e_html_editor_view_get_html_mode (view)) + e_html_editor_view_quote_plain_text_element (view, element); + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + node = webkit_dom_range_get_end_container (range, NULL); + + webkit_dom_node_append_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (blockquote), + NULL); + + e_html_editor_selection_restore_caret_position (selection); + + e_html_editor_view_force_spell_check_for_current_paragraph (view); + + g_free (escaped_text); +} + +static void +html_editor_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHANGED: + e_html_editor_view_set_changed ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_HTML_MODE: + e_html_editor_view_set_html_mode ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_INLINE_SPELLING: + e_html_editor_view_set_inline_spelling ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_LINKS: + e_html_editor_view_set_magic_links ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_SMILEYS: + e_html_editor_view_set_magic_smileys ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CAN_COPY: + g_value_set_boolean ( + value, webkit_web_view_can_copy_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_CUT: + g_value_set_boolean ( + value, webkit_web_view_can_cut_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_PASTE: + g_value_set_boolean ( + value, webkit_web_view_can_paste_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_REDO: + g_value_set_boolean ( + value, webkit_web_view_can_redo ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_UNDO: + g_value_set_boolean ( + value, webkit_web_view_can_undo ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CHANGED: + g_value_set_boolean ( + value, e_html_editor_view_get_changed ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_HTML_MODE: + g_value_set_boolean ( + value, e_html_editor_view_get_html_mode ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_INLINE_SPELLING: + g_value_set_boolean ( + value, e_html_editor_view_get_inline_spelling ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_MAGIC_LINKS: + g_value_set_boolean ( + value, e_html_editor_view_get_magic_links ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_MAGIC_SMILEYS: + g_value_set_boolean ( + value, e_html_editor_view_get_magic_smileys ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_SPELL_CHECKER: + g_value_set_object ( + value, e_html_editor_view_get_spell_checker ( + E_HTML_EDITOR_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_view_dispose (GObject *object) +{ + EHTMLEditorViewPrivate *priv; + + priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (object); + + g_clear_object (&priv->selection); + + if (priv->convertor_web_view != NULL) { + g_object_unref (priv->convertor_web_view); + priv->convertor_web_view = NULL; + } + + if (priv->aliasing_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->aliasing_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->aliasing_settings); + priv->aliasing_settings = NULL; + } + + if (priv->font_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->font_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->font_settings); + priv->font_settings = NULL; + } + + g_hash_table_remove_all (priv->inline_images); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->dispose (object); +} + +static void +html_editor_view_finalize (GObject *object) +{ + EHTMLEditorViewPrivate *priv; + + priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (object); + + g_hash_table_destroy (priv->inline_images); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->finalize (object); +} + +static void +html_editor_view_constructed (GObject *object) +{ + e_extensible_load_extensions (E_EXTENSIBLE (object)); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->constructed (object); +} + +static void +html_editor_view_save_element_under_mouse_click (GtkWidget *widget) +{ + gint x, y; + GdkDeviceManager *device_manager; + GdkDevice *pointer; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *element; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (widget)); + + device_manager = gdk_display_get_device_manager ( + gtk_widget_get_display (GTK_WIDGET (widget))); + pointer = gdk_device_manager_get_client_pointer (device_manager); + gdk_window_get_device_position ( + gtk_widget_get_window (GTK_WIDGET (widget)), pointer, &x, &y, NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (widget)); + element = webkit_dom_document_element_from_point (document, x, y); + + view = E_HTML_EDITOR_VIEW (widget); + view->priv->element_under_mouse = element; +} + +static gboolean +html_editor_view_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + gboolean event_handled; + + if (event->button == 2) { + /* Middle click paste */ + g_signal_emit (widget, signals[PASTE_PRIMARY_CLIPBOARD], 0); + event_handled = TRUE; + } else if (event->button == 3) { + html_editor_view_save_element_under_mouse_click (widget); + g_signal_emit ( + widget, signals[POPUP_EVENT], + 0, event, &event_handled); + } else { + event_handled = FALSE; + } + + if (event_handled) + return TRUE; + + /* Chain up to parent's button_press_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + button_press_event (widget, event); +} + +static gboolean +html_editor_view_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + WebKitWebView *webview; + WebKitHitTestResult *hit_test; + WebKitHitTestResultContext context; + gchar *uri; + + webview = WEBKIT_WEB_VIEW (widget); + hit_test = webkit_web_view_get_hit_test_result (webview, event); + + g_object_get ( + hit_test, + "context", &context, + "link-uri", &uri, + NULL); + + g_object_unref (hit_test); + + /* Left click on a link */ + if ((context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) && + (event->button == 1)) { + + /* Ctrl + Left Click on link opens it, otherwise ignore the + * click completely */ + if (event->state & GDK_CONTROL_MASK) { + GtkWidget *toplevel; + GdkScreen *screen; + + toplevel = gtk_widget_get_toplevel (widget); + screen = gtk_window_get_screen (GTK_WINDOW (toplevel)); + gtk_show_uri (screen, uri, event->time, NULL); + g_free (uri); + } + + return TRUE; + } + + g_free (uri); + + /* Chain up to parent's button_release_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + button_release_event (widget, event); +} + +static gboolean +insert_new_line_into_citation (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gboolean html_mode, ret_val; + + html_mode = e_html_editor_view_get_html_mode (view); + selection = e_html_editor_view_get_selection (view); + + ret_val = e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, NULL); + + if (ret_val && !html_mode) { + WebKitDOMElement *element; + WebKitDOMDocument *document; + WebKitDOMNode *next_sibling; + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + element = webkit_dom_document_query_selector ( + document, "body>br", NULL); + + next_sibling = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (element)); + + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (next_sibling)) { + /* Quote content */ + next_sibling = WEBKIT_DOM_NODE ( + e_html_editor_view_quote_plain_text_element ( + view, WEBKIT_DOM_ELEMENT (next_sibling))); + /* Renew spellcheck */ + e_html_editor_view_force_spell_check (view); + /* Insert caret node on right position */ + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (element)), + e_html_editor_selection_get_caret_position_node ( + document), + WEBKIT_DOM_NODE (element), + NULL); + /* Restore caret position */ + e_html_editor_selection_restore_caret_position ( + selection); + } + } + + return ret_val; +} + +static gboolean +prevent_from_deleting_last_element_in_body (EHTMLEditorView *view) +{ + gboolean ret_val = FALSE; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + list = webkit_dom_node_get_child_nodes (WEBKIT_DOM_NODE (body)); + + if (webkit_dom_node_list_get_length (list) <= 1) { + gchar *content; + + content = webkit_dom_node_get_text_content (WEBKIT_DOM_NODE (body)); + + if (!*content) + ret_val = TRUE; + + g_free (content); + + if (webkit_dom_element_query_selector (WEBKIT_DOM_ELEMENT (body), "img", NULL)) + ret_val = FALSE; + } + + return ret_val; +} + +static gboolean +html_editor_view_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + EHTMLEditorView *view = E_HTML_EDITOR_VIEW (widget); + + if (event->keyval == GDK_KEY_Tab) + return e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, "\t"); + + if ((event->keyval == GDK_KEY_Control_L) || + (event->keyval == GDK_KEY_Control_R)) { + + html_editor_view_set_links_active (view, TRUE); + } + + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_KP_Enter)) { + EHTMLEditorSelection *selection; + + selection = e_html_editor_view_get_selection (view); + /* When user presses ENTER in a citation block, WebKit does + * not break the citation automatically, so we need to use + * the special command to do it. */ + if (e_html_editor_selection_is_citation (selection)) + return insert_new_line_into_citation (view); + } + + /* BackSpace in indented block decrease indent level by one */ + if (event->keyval == GDK_KEY_BackSpace) { + EHTMLEditorSelection *selection; + + selection = e_html_editor_view_get_selection (view); + if (e_html_editor_selection_is_indented (selection)) { + WebKitDOMElement *caret; + + caret = e_html_editor_selection_save_caret_position (selection); + + if (!webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (caret))) { + e_html_editor_selection_clear_caret_position_marker (selection); + e_html_editor_selection_unindent (selection); + return TRUE; + } else + e_html_editor_selection_clear_caret_position_marker (selection); + } + + if (prevent_from_deleting_last_element_in_body (view)) + return TRUE; + } + + /* Chain up to parent's key_press_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + key_press_event (widget, event); +} + +static void +mark_node_as_paragraph_after_ending_list (EHTMLEditorSelection *selection, + WebKitDOMDocument *document) +{ + gint ii, length; + WebKitDOMNodeList *list; + + /* When pressing Enter on empty line in the list WebKit will end that + * list and inserts <div><br></div> so mark it for wrapping */ + list = webkit_dom_document_query_selector_all ( + document, "body > div:not(.-x-evo-paragraph) > br", NULL); + + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_get_parent_node ( + webkit_dom_node_list_item (list, ii)); + + e_html_editor_selection_set_paragraph_style ( + selection, WEBKIT_DOM_ELEMENT (node), -1, 0, ""); + } +} + +static gboolean +surround_text_with_paragraph_if_needed (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + WebKitDOMNode *node) +{ + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (node); + WebKitDOMNode *prev_sibling = webkit_dom_node_get_previous_sibling (node); + WebKitDOMElement *element; + + /* All text in composer has to be written in div elements, so if + * we are writing something straight to the body, surround it with + * paragraph */ + if (WEBKIT_DOM_IS_TEXT (node) && + WEBKIT_DOM_IS_HTML_BODY_ELEMENT (webkit_dom_node_get_parent_node (node))) { + element = e_html_editor_selection_put_node_into_paragraph ( + selection, + document, + node, + e_html_editor_selection_get_caret_position_node (document)); + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (next_sibling), + next_sibling, + NULL); + } + + /* Tab character */ + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "Apple-tab-span")) { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (element), + prev_sibling, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)), + NULL); + } + + return TRUE; + } + + return FALSE; +} + +static gboolean +html_editor_view_key_release_event (GtkWidget *widget, + GdkEventKey *event) +{ + WebKitDOMDocument *document; + WebKitDOMRange *range; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + view = E_HTML_EDITOR_VIEW (widget); + range = html_editor_view_get_dom_range (view); + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (widget)); + + if (view->priv->magic_smileys && + view->priv->html_mode) { + html_editor_view_check_magic_smileys (view, range); + } + + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_Linefeed) || + (event->keyval == GDK_KEY_KP_Enter) || + (event->keyval == GDK_KEY_space)) { + + html_editor_view_check_magic_links (view, range, FALSE, event); + + mark_node_as_paragraph_after_ending_list (selection, document); + } else { + WebKitDOMNode *node; + + node = webkit_dom_range_get_end_container (range, NULL); + + if (surround_text_with_paragraph_if_needed (selection, document, node)) { + e_html_editor_selection_restore_caret_position (selection); + node = webkit_dom_range_get_end_container (range, NULL); + range = html_editor_view_get_dom_range (view); + } + + if (WEBKIT_DOM_IS_TEXT (node)) { + gchar *text; + + text = webkit_dom_node_get_text_content (node); + + if (g_strcmp0 (text, "") != 0 && !g_unichar_isspace (g_utf8_get_char (text))) { + WebKitDOMNode *prev_sibling; + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + + if (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling)) + html_editor_view_check_magic_links (view, range, FALSE, event); + } + g_free (text); + } + } + + if ((event->keyval == GDK_KEY_Control_L) || + (event->keyval == GDK_KEY_Control_R)) { + + html_editor_view_set_links_active (view, FALSE); + } + + /* Chain up to parent's key_release_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + key_release_event (widget, event); +} + +static void +html_editor_view_paste_clipboard_quoted (EHTMLEditorView *view) +{ + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get_for_display ( + gdk_display_get_default (), + GDK_SELECTION_CLIPBOARD); + + gtk_clipboard_request_text ( + clipboard, + (GtkClipboardTextReceivedFunc) clipboard_text_received, + view); +} + +static gboolean +html_editor_view_image_exists_in_cache (const gchar *image_uri) +{ + gchar *filename; + gchar *hash; + gboolean exists = FALSE; + + g_return_val_if_fail (emd_global_http_cache != NULL, FALSE); + + hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, image_uri, -1); + filename = camel_data_cache_get_filename ( + emd_global_http_cache, "http", hash); + + if (filename != NULL) { + exists = g_file_test (filename, G_FILE_TEST_EXISTS); + g_free (filename); + } + + g_free (hash); + + return exists; +} + +static gchar * +html_editor_view_redirect_uri (EHTMLEditorView *view, + const gchar *uri) +{ + EImageLoadingPolicy image_policy; + GSettings *settings; + gboolean uri_is_http; + + uri_is_http = + g_str_has_prefix (uri, "http:") || + g_str_has_prefix (uri, "https:") || + g_str_has_prefix (uri, "evo-http:") || + g_str_has_prefix (uri, "evo-https:"); + + /* Redirect http(s) request to evo-http(s) protocol. + * See EMailRequest for further details about this. */ + if (uri_is_http) { + gchar *new_uri; + SoupURI *soup_uri; + gboolean image_exists; + + /* Check Evolution's cache */ + image_exists = html_editor_view_image_exists_in_cache (uri); + + settings = g_settings_new ("org.gnome.evolution.mail"); + image_policy = g_settings_get_enum (settings, "image-loading-policy"); + g_object_unref (settings); + /* If the URI is not cached and we are not allowed to load it + * then redirect to invalid URI, so that webkit would display + * a native placeholder for it. */ + if (!image_exists && (image_policy == E_IMAGE_LOADING_POLICY_NEVER)) { + return g_strdup ("about:blank"); + } + + new_uri = g_strconcat ("evo-", uri, NULL); + soup_uri = soup_uri_new (new_uri); + g_free (new_uri); + + new_uri = soup_uri_to_string (soup_uri, FALSE); + + soup_uri_free (soup_uri); + + return new_uri; + } + + return g_strdup (uri); +} + +static void +html_editor_view_resource_requested (WebKitWebView *web_view, + WebKitWebFrame *frame, + WebKitWebResource *resource, + WebKitNetworkRequest *request, + WebKitNetworkResponse *response, + gpointer user_data) +{ + const gchar *original_uri; + + original_uri = webkit_network_request_get_uri (request); + + if (original_uri != NULL) { + gchar *redirected_uri; + + redirected_uri = html_editor_view_redirect_uri ( + E_HTML_EDITOR_VIEW (web_view), original_uri); + + webkit_network_request_set_uri (request, redirected_uri); + + g_free (redirected_uri); + } +} + +static void +e_html_editor_view_class_init (EHTMLEditorViewClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = html_editor_view_get_property; + object_class->set_property = html_editor_view_set_property; + object_class->dispose = html_editor_view_dispose; + object_class->finalize = html_editor_view_finalize; + object_class->constructed = html_editor_view_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = html_editor_view_button_press_event; + widget_class->button_release_event = html_editor_view_button_release_event; + widget_class->key_press_event = html_editor_view_key_press_event; + widget_class->key_release_event = html_editor_view_key_release_event; + + class->paste_clipboard_quoted = html_editor_view_paste_clipboard_quoted; + + /** + * EHTMLEditorView:can-copy + * + * Determines whether it's possible to copy to clipboard. The action + * is usually disabled when there is no selection to copy. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_COPY, + g_param_spec_boolean ( + "can-copy", + "Can Copy", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-cut + * + * Determines whether it's possible to cut to clipboard. The action + * is usually disabled when there is no selection to cut. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_CUT, + g_param_spec_boolean ( + "can-cut", + "Can Cut", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-paste + * + * Determines whether it's possible to paste from clipboard. The action + * is usually disabled when there is no valid content in clipboard to + * paste. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_PASTE, + g_param_spec_boolean ( + "can-paste", + "Can Paste", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-redo + * + * Determines whether it's possible to redo previous action. The action + * is usually disabled when there is no action to redo. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_REDO, + g_param_spec_boolean ( + "can-redo", + "Can Redo", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-undo + * + * Determines whether it's possible to undo last action. The action + * is usually disabled when there is no previous action to undo. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_UNDO, + g_param_spec_boolean ( + "can-undo", + "Can Undo", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:changed + * + * Determines whether document has been modified + */ + g_object_class_install_property ( + object_class, + PROP_CHANGED, + g_param_spec_boolean ( + "changed", + _("Changed property"), + _("Whether editor changed"), + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:html-mode + * + * Determines whether HTML or plain text mode is enabled. + **/ + g_object_class_install_property ( + object_class, + PROP_HTML_MODE, + g_param_spec_boolean ( + "html-mode", + "HTML Mode", + "Edit HTML or plain text", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView::inline-spelling + * + * Determines whether automatic spellchecking is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_INLINE_SPELLING, + g_param_spec_boolean ( + "inline-spelling", + "Inline Spelling", + "Check your spelling as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:magic-links + * + * Determines whether automatic conversion of text links into + * HTML links is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_MAGIC_LINKS, + g_param_spec_boolean ( + "magic-links", + "Magic Links", + "Make URIs clickable as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:magic-smileys + * + * Determines whether automatic conversion of text smileys into + * images is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_MAGIC_SMILEYS, + g_param_spec_boolean ( + "magic-smileys", + "Magic Smileys", + "Convert emoticons to images as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:spell-checker: + * + * The #ESpellChecker used for spell checking. + **/ + g_object_class_install_property ( + object_class, + PROP_SPELL_CHECKER, + g_param_spec_object ( + "spell-checker", + "Spell Checker", + "The spell checker", + E_TYPE_SPELL_CHECKER, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:popup-event + * + * Emitted whenever a context menu is requested. + */ + signals[POPUP_EVENT] = g_signal_new ( + "popup-event", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EHTMLEditorViewClass, popup_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__BOXED, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + /** + * EHTMLEditorView:paste-primary-clipboad + * + * Emitted when user presses middle button on EHTMLEditorView + */ + signals[PASTE_PRIMARY_CLIPBOARD] = g_signal_new ( + "paste-primary-clipboard", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EHTMLEditorViewClass, paste_primary_clipboard), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static GString * +replace_string_spaces_with_nbsp_in_prefix (const gchar *text) +{ + GString *str; + gint counter = 0; + + g_return_val_if_fail (text != NULL, NULL); + + str = g_string_new (""); + + while (g_str_has_prefix (text + counter, " ")) { + g_string_append (str, " "); + + counter++; + } + + g_string_append (str, text + counter); + + return str; +} + +/* This parses the HTML code (that contains just text, and BR elements) + * into paragraphs. + * HTML code in that format we can get by taking innerText from some element, + * setting it to another one and finally getting innerHTML from it */ +static void +parse_html_into_paragraphs (EHTMLEditorView *view, + WebKitDOMDocument *document, + WebKitDOMElement *blockquote, + const gchar *html, + gboolean use_pre) +{ + const gchar *prev_br, *next_br; + gchar *inner_html; + gint citation_level = 0; + GString *start, *end; + gboolean ignore_next_br = FALSE; + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), "", NULL); + + prev_br = html; + next_br = strstr (prev_br, "<br>"); + + while (next_br) { + gboolean local_ignore_next_br = ignore_next_br; + const gchar *citation = NULL, *citation_end = NULL; + const gchar *rest = NULL, *with_br = NULL; + gchar *to_insert = NULL; + WebKitDOMElement *paragraph; + + to_insert = g_utf8_substring ( + prev_br, 0, g_utf8_pointer_to_offset (prev_br, next_br)); + + with_br = strstr (to_insert, "<br>"); + + ignore_next_br = FALSE; + + citation = strstr (to_insert, "##CITATION_"); + if (citation) { + if (strstr (to_insert, "##CITATION_START##")) + citation_level++; + else + citation_level--; + + citation_end = strstr (citation + 2, "##"); + if (citation_end) + rest = citation_end + 2; + } else { + rest = with_br ? + to_insert + 4 + (with_br - to_insert) : to_insert; + } + + if (use_pre) { + paragraph = webkit_dom_document_create_element ( + document, "pre", NULL); + } else { + paragraph = e_html_editor_selection_get_paragraph_element ( + e_html_editor_view_get_selection (view), + document, -1, citation_level); + } + + if (with_br && !*rest && !citation &&!local_ignore_next_br) { + WebKitDOMNode *paragraph_clone; + + paragraph_clone = webkit_dom_node_clone_node ( + WEBKIT_DOM_NODE (paragraph), TRUE); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph_clone), + " ", + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + paragraph_clone, + NULL); + } + + if (citation) { + WebKitDOMText *text; + gchar *citation_mark; + + citation_mark = g_utf8_substring ( + citation, 0, + g_utf8_pointer_to_offset ( + citation, citation_end + 2)); + + text = webkit_dom_document_create_text_node ( + document, citation_mark); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (text), + NULL); + + g_free (citation_mark); + } + + if (rest && *rest){ + GString *space_to_nbsp; + + space_to_nbsp = replace_string_spaces_with_nbsp_in_prefix (rest); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), + space_to_nbsp->str, + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (paragraph), + NULL); + g_string_free (space_to_nbsp, TRUE); + } + + if (citation_end) + ignore_next_br = TRUE; + + prev_br = next_br; + next_br = strstr (prev_br + 4, "<br>"); + g_free (to_insert); + } + + if (g_utf8_strlen (prev_br, -1) > 0) { + WebKitDOMElement *paragraph; + + if (use_pre) { + paragraph = webkit_dom_document_create_element ( + document, "pre", NULL); + } else { + paragraph = e_html_editor_selection_get_paragraph_element ( + e_html_editor_view_get_selection (view), + document, -1, citation_level); + } + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), + g_str_has_prefix (prev_br, "<br>") ? prev_br + 4 : prev_br, + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (paragraph), + NULL); + } + + /* Replace text markers with actual HTML blockquotes */ + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote)); + start = e_str_replace_string ( + inner_html, "##CITATION_START##","<blockquote type=\"cite\">"); + end = e_str_replace_string ( + start->str, "##CITATION_END##", "</blockquote>"); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), end->str, NULL); + + g_free (inner_html); + g_string_free (start, TRUE); + g_string_free (end, TRUE); +} + +static void +mark_citation (WebKitDOMElement *citation) +{ + gchar *inner_html, *surrounded; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (citation)); + + surrounded = g_strconcat ( + "<span>##CITATION_START##</span>", inner_html, + "<span>##CITATION_END##</span>", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (citation), surrounded, NULL); + + element_add_class (citation, "marked"); + + g_free (inner_html); + g_free (surrounded); +} + +static gint +create_text_markers_for_citations_in_document (WebKitDOMDocument *document) +{ + gint count = 0; + WebKitDOMElement *citation; + + citation = webkit_dom_document_query_selector ( + document, "blockquote[type=cite]:not(.marked)", NULL); + + while (citation) { + mark_citation (citation); + count ++; + + citation = webkit_dom_document_query_selector ( + document, "blockquote[type=cite]:not(.marked)", NULL); + } + + return count; +} + +static gint +create_text_markers_for_citations_in_element (WebKitDOMElement *element) +{ + gint count = 0; + WebKitDOMElement *citation; + + citation = webkit_dom_element_query_selector ( + element, "blockquote[type=cite]:not(.marked)", NULL); + + while (citation) { + mark_citation (citation); + count ++; + + citation = webkit_dom_element_query_selector ( + element, "blockquote[type=cite]:not(.marked)", NULL); + } + + return count; +} + +static void +html_editor_view_process_document_from_convertor (EHTMLEditorView *view, + WebKitDOMDocument *document_convertor) +{ + EHTMLEditorSelection *selection = e_html_editor_view_get_selection (view); + gboolean start_bottom; + gchar *inner_text, *inner_html; + gint ii; + GSettings *settings; + WebKitDOMDocument *document; + WebKitDOMElement *paragraph, *new_blockquote, *top_signature; + WebKitDOMElement *cite_body, *signature; + WebKitDOMHTMLElement *body, *body_convertor; + WebKitDOMNodeList *list; + + settings = g_settings_new ("org.gnome.evolution.mail"); + start_bottom = g_settings_get_boolean (settings, "composer-reply-start-bottom"); + g_object_unref (settings); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + body_convertor = webkit_dom_document_get_body (document_convertor); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted", "", NULL); + + paragraph = webkit_dom_document_get_element_by_id (document, "-x-evo-input-start"); + if (!paragraph) { + paragraph = e_html_editor_selection_get_paragraph_element ( + selection, document, -1, 0); + webkit_dom_element_set_id (paragraph, "-x-evo-input-start"); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), UNICODE_ZERO_WIDTH_SPACE, NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)), + WEBKIT_DOM_NODE (paragraph), + NULL); + } + + list = webkit_dom_document_query_selector_all ( + document_convertor, "span.-x-evo-to-body", NULL); + for (ii = webkit_dom_node_list_get_length (list) - 1; ii >= 0; ii--) { + WebKitDOMNode *node; + + node = webkit_dom_node_list_item (list, ii); + while (webkit_dom_node_has_child_nodes (node)) { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + webkit_dom_node_clone_node ( + webkit_dom_node_get_first_child (node), TRUE), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (paragraph)), + NULL); + + webkit_dom_node_remove_child ( + node, webkit_dom_node_get_first_child (node), NULL); + } + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body_convertor), + WEBKIT_DOM_NODE (node), + NULL); + } + + repair_gmail_blockquotes (document_convertor); + + create_text_markers_for_citations_in_document (document_convertor); + + /* Get innertText from convertor */ + inner_text = webkit_dom_html_element_get_inner_text (body_convertor); + + cite_body = webkit_dom_document_query_selector ( + document_convertor, "span.-x-evo-cite-body", NULL); + + top_signature = webkit_dom_document_query_selector ( + document, ".-x-evo-top-signature", NULL); + signature = webkit_dom_document_query_selector ( + document, "span.-x-evo-signature", NULL); + + if (cite_body) { + if (!(top_signature && start_bottom)) + e_html_editor_selection_save_caret_position (selection); + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (paragraph), + WEBKIT_DOM_NODE ( + e_html_editor_selection_get_caret_position_node ( + document)), + NULL); + } + + new_blockquote = webkit_dom_document_create_element ( + document, "blockquote", NULL); + webkit_dom_element_set_attribute ( + new_blockquote, "type", "cite", NULL); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (new_blockquote), inner_text, NULL); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (new_blockquote)); + + if (cite_body) { + webkit_dom_element_set_attribute ( + new_blockquote, "id", "-x-evo-main-cite", NULL); + + parse_html_into_paragraphs ( + view, document, new_blockquote, inner_html, FALSE); + + if (start_bottom) { + if (signature) { + WebKitDOMNode *parent = + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)); + if (top_signature) { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (paragraph), + e_html_editor_selection_get_caret_position_node ( + document), + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + WEBKIT_DOM_NODE (parent), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (new_blockquote)), + NULL); + } + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (new_blockquote)), + NULL); + } + } else { + if (signature) { + WebKitDOMNode *parent = + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)); + + if (top_signature) { + WebKitDOMElement *br; + + br = webkit_dom_document_create_element ( + document, "BR", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + /* Insert NL after signature */ + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (br), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (paragraph)), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (br), + NULL); + } else + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + WEBKIT_DOM_NODE (parent), + NULL); + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + } + } + } else { + WebKitDOMNode *signature_clone, *first_child; + + if (signature) { + signature_clone = webkit_dom_node_clone_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + TRUE); + } + + parse_html_into_paragraphs ( + view, document, WEBKIT_DOM_ELEMENT (body), + inner_html, FALSE); + + if (signature) { + if (!top_signature) { + signature_clone = webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + signature_clone, + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + signature_clone, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)), + NULL); + } + } + + first_child = webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)); + + webkit_dom_node_insert_before ( + first_child, + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_first_child (first_child), + NULL); + } + + if (!e_html_editor_view_get_html_mode (view)) + e_html_editor_selection_wrap_paragraphs_in_document ( + selection, document); + if (webkit_dom_document_query_selector (document, "blockquote[type=cite]", NULL)) + body = WEBKIT_DOM_HTML_ELEMENT ( + e_html_editor_view_quote_plain_text (view)); + + e_html_editor_selection_restore_caret_position (selection); + e_html_editor_view_force_spell_check (view); + + /* Register on input event that is called when the content (body) is modified */ + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (body), + "input", + G_CALLBACK (body_input_event_cb), + FALSE, + view); + + g_free (inner_html); + g_free (inner_text); +} + +static void +html_editor_view_insert_converted_html_into_selection (EHTMLEditorView *view, + WebKitDOMDocument *document_convertor) +{ + gchar *inner_text, *inner_html; + WebKitDOMDocument *document; + WebKitDOMElement *element; + WebKitDOMHTMLElement *convertor_body; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + convertor_body = webkit_dom_document_get_body (document_convertor); + + inner_text = webkit_dom_html_element_get_inner_text (convertor_body); + element = webkit_dom_document_create_element (document, "div", NULL); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (element), inner_text, NULL); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + parse_html_into_paragraphs ( + view, document, element, inner_html, FALSE); + + g_free (inner_html); + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, inner_html); + + e_html_editor_view_force_spell_check (view); + + g_free (inner_html); + g_free (inner_text); +} + +static void +html_plain_text_convertor_load_status_changed (WebKitWebView *web_view, + GParamSpec *pspec, + EHTMLEditorView *view) +{ + WebKitDOMDocument *document_convertor; + + if (webkit_web_view_get_load_status (web_view) != WEBKIT_LOAD_FINISHED) + return; + + document_convertor = webkit_web_view_get_dom_document (web_view); + + if (view->priv->convertor_insert) + html_editor_view_insert_converted_html_into_selection ( + view, document_convertor); + else + html_editor_view_process_document_from_convertor ( + view, document_convertor); +} + +static void +e_html_editor_view_init (EHTMLEditorView *view) +{ + WebKitWebSettings *settings; + GSettings *g_settings; + GSettingsSchema *settings_schema; + ESpellChecker *checker; + gchar **languages; + gchar *comma_separated; + const gchar *user_cache_dir; + + view->priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (view); + + webkit_web_view_set_editable (WEBKIT_WEB_VIEW (view), TRUE); + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + + g_object_set ( + G_OBJECT (settings), + "enable-developer-extras", TRUE, + "enable-dom-paste", TRUE, + "enable-file-access-from-file-uris", TRUE, + "enable-plugins", FALSE, + "enable-scripts", FALSE, + "enable-spell-checking", TRUE, + "respect-image-orientation", TRUE, + NULL); + + webkit_web_view_set_settings (WEBKIT_WEB_VIEW (view), settings); + + /* Override the spell-checker, use our own */ + checker = e_spell_checker_new (); + webkit_set_text_checker (G_OBJECT (checker)); + g_object_unref (checker); + + /* Don't use CSS when possible to preserve compatibility with older + * versions of Evolution or other MUAs */ + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS, "false"); + + g_signal_connect ( + view, "user-changed-contents", + G_CALLBACK (html_editor_view_user_changed_contents_cb), NULL); + g_signal_connect ( + view, "selection-changed", + G_CALLBACK (html_editor_view_selection_changed_cb), NULL); + g_signal_connect ( + view, "should-show-delete-interface-for-element", + G_CALLBACK (html_editor_view_should_show_delete_interface_for_element), NULL); + g_signal_connect ( + view, "resource-request-starting", + G_CALLBACK (html_editor_view_resource_requested), NULL); + g_signal_connect ( + view, "notify::load-status", + G_CALLBACK (html_editor_view_load_status_changed), NULL); + + view->priv->selection = g_object_new ( + E_TYPE_HTML_EDITOR_SELECTION, + "html-editor-view", view, + NULL); + + g_settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect_swapped ( + g_settings, "changed::font-name", + G_CALLBACK (e_html_editor_view_update_fonts), view); + g_signal_connect_swapped ( + g_settings, "changed::monospace-font-name", + G_CALLBACK (e_html_editor_view_update_fonts), view); + view->priv->font_settings = g_settings; + + /* This schema is optional. Use if available. */ + settings_schema = g_settings_schema_source_lookup ( + g_settings_schema_source_get_default (), + "org.gnome.settings-daemon.plugins.xsettings", FALSE); + if (settings_schema != NULL) { + g_settings = g_settings_new ("org.gnome.settings-daemon.plugins.xsettings"); + g_signal_connect_swapped ( + settings, "changed::antialiasing", + G_CALLBACK (e_html_editor_view_update_fonts), view); + view->priv->aliasing_settings = g_settings; + } + + view->priv->inline_images = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + e_html_editor_view_update_fonts (view); + + /* Give spell check languages to WebKit */ + languages = e_spell_checker_list_active_languages (checker, NULL); + comma_separated = g_strjoinv (",", languages); + g_strfreev (languages); + + g_object_set ( + G_OBJECT (settings), + "spell-checking-languages", comma_separated, + NULL); + + g_free (comma_separated); + + view->priv->convertor_insert = FALSE; + + view->priv->convertor_web_view = + g_object_ref_sink (WEBKIT_WEB_VIEW (webkit_web_view_new ())); + settings = webkit_web_view_get_settings (view->priv->convertor_web_view); + + g_object_set ( + G_OBJECT (settings), + "enable-scripts", FALSE, + "enable-plugins", FALSE, + NULL); + + g_signal_connect ( + view->priv->convertor_web_view, "notify::load-status", + G_CALLBACK (html_plain_text_convertor_load_status_changed), view); + + /* Make WebKit think we are displaying a local file, so that it + * does not block loading resources from file:// protocol */ + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), "", "text/html", "UTF-8", "file://"); + + html_editor_view_set_links_active (view, FALSE); + + if (emd_global_http_cache == NULL) { + user_cache_dir = e_get_user_cache_dir (); + emd_global_http_cache = camel_data_cache_new (user_cache_dir, NULL); + + /* cache expiry - 2 hour access, 1 day max */ + camel_data_cache_set_expire_age ( + emd_global_http_cache, 24 * 60 * 60); + camel_data_cache_set_expire_access ( + emd_global_http_cache, 2 * 60 * 60); + } +} + +/** + * e_html_editor_view_new: + * + * Returns a new instance of the editor. + * + * Returns: A newly created #EHTMLEditorView. [transfer-full] + */ +EHTMLEditorView * +e_html_editor_view_new (void) +{ + return g_object_new (E_TYPE_HTML_EDITOR_VIEW, NULL); +} + +/** + * e_html_editor_view_get_selection: + * @view: an #EHTMLEditorView + * + * Returns an #EHTMLEditorSelection object which represents current selection or + * cursor position within the editor document. The #EHTMLEditorSelection allows + * programmer to manipulate with formatting, selection, styles etc. + * + * Returns: An always valid #EHTMLEditorSelection object. The object is owned by + * the @view and should never be free'd. + */ +EHTMLEditorSelection * +e_html_editor_view_get_selection (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), NULL); + + return view->priv->selection; +} + +/** + * e_html_editor_view_exec_command: + * @view: an #EHTMLEditorView + * @command: an #EHTMLEditorViewCommand to execute + * @value: value of the command (or @NULL if the command does not require value) + * + * The function will fail when @value is @NULL or empty but the current @command + * requires a value to be passed. The @value is ignored when the @command does + * not expect any value. + * + * Returns: @TRUE when the command was succesfully executed, @FALSE otherwise. + */ +gboolean +e_html_editor_view_exec_command (EHTMLEditorView *view, + EHTMLEditorViewCommand command, + const gchar *value) +{ + WebKitDOMDocument *document; + const gchar *cmd_str = 0; + gboolean has_value; + + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + +#define CHECK_COMMAND(cmd,str,val) case cmd:\ + if (val) {\ + g_return_val_if_fail (value && *value, FALSE);\ + }\ + has_value = val; \ + cmd_str = str;\ + break; + + switch (command) { + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_BACKGROUND_COLOR, "BackColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_BOLD, "Bold", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_COPY, "Copy", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_CREATE_LINK, "CreateLink", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_CUT, "Cut", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_DEFAULT_PARAGRAPH_SEPARATOR, "DefaultParagraphSeparator", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_DELETE, "Delete", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FIND_STRING, "FindString", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_NAME, "FontName", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE, "FontSize", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE_DELTA, "FontSizeDelta", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORE_COLOR, "ForeColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORMAT_BLOCK, "FormatBlock", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORWARD_DELETE, "ForwardDelete", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_HILITE_COLOR, "HiliteColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INDENT, "Indent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_HORIZONTAL_RULE, "InsertHorizontalRule", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, "InsertHTML", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_IMAGE, "InsertImage", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_LINE_BREAK, "InsertLineBreak", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, "InsertNewlineInQuotedContent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_ORDERED_LIST, "InsertOrderedList", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_PARAGRAPH, "InsertParagraph", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, "InsertText", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_UNORDERED_LIST, "InsertUnorderedList", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_ITALIC, "Italic", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_CENTER, "JustifyCenter", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_FULL, "JustifyFull", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_LEFT, "JustifyLeft", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_NONE, "JustifyNone", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_RIGHT, "JustifyRight", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_OUTDENT, "Outdent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE, "Paste", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE_AND_MATCH_STYLE, "PasteAndMatchStyle", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE_AS_PLAIN_TEXT, "PasteAsPlainText", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PRINT, "Print", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_REDO, "Redo", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_REMOVE_FORMAT, "RemoveFormat", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SELECT_ALL, "SelectAll", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_STRIKETHROUGH, "Strikethrough", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS, "StyleWithCSS", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SUBSCRIPT, "Subscript", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SUPERSCRIPT, "Superscript", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_TRANSPOSE, "Transpose", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNDERLINE, "Underline", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNDO, "Undo", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNLINK, "Unlink", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNSELECT, "Unselect", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_USE_CSS, "UseCSS", TRUE) + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + return webkit_dom_document_exec_command ( + document, cmd_str, FALSE, has_value ? value : "" ); +} + +/** + * e_html_editor_view_get_changed: + * @view: an #EHTMLEditorView + * + * Whether content of the editor has been changed. + * + * Returns: @TRUE when document was changed, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_changed (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->changed; +} + +/** + * e_html_editor_view_set_changed: + * @view: an #EHTMLEditorView + * @changed: whether document has been changed or not + * + * Sets whether document has been changed or not. The editor is tracking changes + * automatically, but sometimes it's necessary to change the dirty flag to force + * "Save changes" dialog for example. + */ +void +e_html_editor_view_set_changed (EHTMLEditorView *view, + gboolean changed) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->changed == changed) + return; + + view->priv->changed = changed; + + g_object_notify (G_OBJECT (view), "changed"); +} + +static gboolean +is_citation_node (WebKitDOMNode *node) +{ + char *value; + + if (!WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (node)) + return FALSE; + + value = webkit_dom_element_get_attribute (WEBKIT_DOM_ELEMENT (node), "type"); + + /* citation == <blockquote type='cite'> */ + if (g_strcmp0 (value, "cite") == 0) { + g_free (value); + return TRUE; + } else { + g_free (value); + return FALSE; + } +} + +static gchar * +get_quotation_for_level (gint quote_level) +{ + gint ii; + GString *output = g_string_new (""); + + for (ii = 0; ii < quote_level; ii++) { + g_string_append (output, "<span class=\"quote_character\">"); + g_string_append (output, QUOTE_SYMBOL); + g_string_append (output, " "); + g_string_append (output, "</span>"); + } + + return g_string_free (output, FALSE); +} + +static void +insert_quote_symbols (WebKitDOMHTMLElement *element, + gint quote_level, + gboolean skip_first, + gboolean insert_newline) +{ + gchar *text; + gint ii; + GString *output; + gchar *quotation; + + if (!WEBKIT_DOM_IS_HTML_ELEMENT (element)) + return; + + text = webkit_dom_html_element_get_inner_html (element); + output = g_string_new (""); + quotation = get_quotation_for_level (quote_level); + + if (g_strcmp0 (text, "\n") == 0) { + g_string_append (output, "<span class=\"-x-evo-quoted\">"); + g_string_append (output, quotation); + g_string_append (output, "</span>"); + g_string_append (output, "\n"); + } else { + gchar **lines; + + lines = g_strsplit (text, "\n", 0); + + for (ii = 0; lines[ii]; ii++) { + if (ii == 0 && skip_first) { + if (g_strv_length (lines) == 1) { + g_strfreev (lines); + goto exit; + } + g_string_append (output, lines[ii]); + g_string_append (output, "\n"); + } + + g_string_append (output, "<span class=\"-x-evo-quoted\">"); + g_string_append (output, quotation); + g_string_append (output, "</span>"); + + /* Insert line of text */ + g_string_append (output, lines[ii]); + if ((ii == g_strv_length (lines) - 1) && + !g_str_has_suffix (text, "\n") && !insert_newline) { + /* If we are on last line and node's text doesn't + * end with \n, don't insert it */ + break; + } + g_string_append (output, "\n"); + } + + g_strfreev (lines); + } + + webkit_dom_html_element_set_inner_html (element, output->str, NULL); + exit: + g_free (quotation); + g_free (text); + g_string_free (output, TRUE); +} + +static void +quote_node (WebKitDOMDocument *document, + WebKitDOMNode *node, + gint quote_level) +{ + gboolean skip_first = FALSE; + gboolean insert_newline = FALSE; + gboolean is_html_node = FALSE; + WebKitDOMElement *wrapper; + WebKitDOMNode *node_clone, *prev_sibling, *next_sibling; + + /* Don't quote when we are not in citation */ + if (quote_level == 0) + return; + + if (WEBKIT_DOM_IS_COMMENT (node)) + return; + + if (WEBKIT_DOM_IS_HTML_ELEMENT (node)) { + insert_quote_symbols ( + WEBKIT_DOM_HTML_ELEMENT (node), quote_level, FALSE, FALSE); + return; + } + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + next_sibling = webkit_dom_node_get_next_sibling (node); + + is_html_node = + !WEBKIT_DOM_IS_COMMENT (prev_sibling) && ( + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling) || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "b") || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "i") || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "u")); + + if (prev_sibling && is_html_node) + skip_first = TRUE; + + /* Skip the BR between first blockquote and pre */ + if (quote_level == 1 && next_sibling && WEBKIT_DOM_IS_HTML_PRE_ELEMENT (next_sibling)) + return; + + if (next_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling) && + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (webkit_dom_node_get_next_sibling (next_sibling))) { + insert_newline = TRUE; + } + + /* Do temporary wrapper */ + wrapper = webkit_dom_document_create_element (document, "SPAN", NULL); + webkit_dom_element_set_class_name (wrapper, "-x-evo-temp-text-wrapper"); + + node_clone = webkit_dom_node_clone_node (node, TRUE); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (wrapper), + node_clone, + NULL); + + insert_quote_symbols ( + WEBKIT_DOM_HTML_ELEMENT (wrapper), + quote_level, + skip_first, + insert_newline); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (wrapper), + node, + NULL); +} + +static void +insert_quote_symbols_before_node (WebKitDOMDocument *document, + WebKitDOMNode *node, + gint quote_level, + gboolean is_html_node) +{ + gchar *quotation; + WebKitDOMElement *element; + + quotation = get_quotation_for_level (quote_level); + element = webkit_dom_document_create_element (document, "SPAN", NULL); + element_add_class (element, "-x-evo-quoted"); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element), quotation, NULL); + + if (is_html_node) { + WebKitDOMElement *new_br; + + new_br = webkit_dom_document_create_element (document, "br", NULL); + element_add_class (new_br, "-x-evo-temp-br"); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (new_br), + node, + NULL); + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + node, + NULL); + + if (is_html_node) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + } + + g_free (quotation); +} + +static void +quote_plain_text_recursive (WebKitDOMDocument *document, + WebKitDOMNode *node, + WebKitDOMNode *start_node, + gint quote_level) +{ + gboolean skip_node = FALSE; + gboolean move_next = FALSE; + gboolean suppress_next = FALSE; + gboolean is_html_node = FALSE; + WebKitDOMNode *next_sibling, *prev_sibling; + + node = webkit_dom_node_get_first_child (node); + + while (node) { + skip_node = FALSE; + move_next = FALSE; + is_html_node = FALSE; + + if (WEBKIT_DOM_IS_COMMENT (node)) + goto next_node; + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + next_sibling = webkit_dom_node_get_next_sibling (node); + + if (WEBKIT_DOM_IS_TEXT (node)) { + /* Start quoting after we are in blockquote */ + if (quote_level > 0 && !suppress_next) { + /* When quoting text node, we are wrappering it and + * afterwards replacing it with that wrapper, thus asking + * for next_sibling after quoting will return NULL bacause + * that node don't exist anymore */ + quote_node (document, node, quote_level); + node = next_sibling; + skip_node = TRUE; + } else + suppress_next = FALSE; + + goto next_node; + } + + if (!(WEBKIT_DOM_IS_ELEMENT (node) || WEBKIT_DOM_IS_HTML_ELEMENT (node))) + goto next_node; + + if (element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-caret-position")) { + if (quote_level > 0) + element_add_class ( + WEBKIT_DOM_ELEMENT (node), "-x-evo-caret-quoting"); + + move_next = TRUE; + suppress_next = TRUE; + goto next_node; + } + + if (element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-selection-start-marker") || + element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-selection-end-marker")) { + move_next = TRUE; + suppress_next = TRUE; + goto next_node; + } + + if (WEBKIT_DOM_IS_HTML_META_ELEMENT (node)) { + goto next_node; + } + if (WEBKIT_DOM_IS_HTML_STYLE_ELEMENT (node)) { + move_next = TRUE; + goto next_node; + } + if (WEBKIT_DOM_IS_HTML_IMAGE_ELEMENT (node)) { + move_next = TRUE; + goto next_node; + } + + if (webkit_dom_element_get_child_element_count (WEBKIT_DOM_ELEMENT (node)) != 0) + goto with_children; + + /* Even in plain text mode we can have some basic html element + * like anchor and others. When Forwaring e-mail as Quoted EMFormat + * generates header that contatains <b> tags (bold font). + * We have to treat these elements separately to avoid + * modifications of theirs inner texts */ + is_html_node = + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (node) || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "b") || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "i") || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "u"); + + if (is_html_node) { + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) + insert_quote_symbols_before_node ( + document, prev_sibling, quote_level, TRUE); + + move_next = TRUE; + goto next_node; + } + + /* If element doesn't have children, we can quote it */ + if (is_citation_node (node)) { + /* Citation with just text inside */ + quote_node (document, node, quote_level + 1); + /* Set citation as quoted */ + element_add_class ( + WEBKIT_DOM_ELEMENT (node), + "-x-evo-plaintext-quoted"); + + move_next = TRUE; + goto next_node; + } + + if (!WEBKIT_DOM_IS_HTMLBR_ELEMENT (node)) + goto not_br; + + if (!prev_sibling) { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node (node); + + /* BR in the beginning of the citation */ + if (WEBKIT_DOM_IS_HTML_PRE_ELEMENT (parent)) + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (next_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "-x-evo-temp-text-wrapper")) { + /* Situation when anchors are alone on line */ + gchar *text_content; + + text_content = webkit_dom_node_get_text_content (prev_sibling); + + if (g_str_has_suffix (text_content, "\n")) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + g_free (text_content); + node = next_sibling; + skip_node = TRUE; + goto next_node; + } + g_free (text_content); + } + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) { + gchar *quotation, *content; + + quotation = get_quotation_for_level (quote_level); + + content = g_strconcat ( + "<span class=\"-x-evo-quoted\">", + quotation, + "</span><br class=\"-x-evo-temp-br\">", + NULL); + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (node), + content, + NULL); + + g_free (content); + g_free (quotation); + + node = next_sibling; + skip_node = TRUE; + goto next_node; + } + + if (!prev_sibling && !next_sibling) { + WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node); + + if (WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent)) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + } + + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "-x-evo-temp-text-wrapper")) { + gchar *text_content; + + text_content = webkit_dom_node_get_text_content (prev_sibling); + if (g_strcmp0 (text_content, "") == 0) + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + + g_free (text_content); + } + + if (is_citation_node (prev_sibling)) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + not_br: + if (g_strcmp0 (webkit_dom_node_get_text_content (node), "") == 0) { + move_next = TRUE; + goto next_node; + } + + quote_node (document, node, quote_level); + + move_next = TRUE; + goto next_node; + + with_children: + if (is_citation_node (node)) { + /* Go deeper and increase level */ + quote_plain_text_recursive ( + document, node, start_node, quote_level + 1); + /* set citation as quoted */ + element_add_class ( + WEBKIT_DOM_ELEMENT (node), + "-x-evo-plaintext-quoted"); + move_next = TRUE; + } else { + quote_plain_text_recursive ( + document, node, start_node, quote_level); + move_next = TRUE; + } + next_node: + if (!skip_node) { + /* Move to next node */ + if (!move_next && webkit_dom_node_has_child_nodes (node)) { + node = webkit_dom_node_get_first_child (node); + } else if (webkit_dom_node_get_next_sibling (node)) { + node = webkit_dom_node_get_next_sibling (node); + } else { + return; + } + } + } +} + +static gint +get_citation_level (WebKitDOMNode *node, + gboolean set_plaintext_quoted) +{ + WebKitDOMNode *parent = node; + gint level = 0; + + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (parent) && + webkit_dom_element_has_attribute (WEBKIT_DOM_ELEMENT (parent), "type")) { + level++; + + if (set_plaintext_quoted) { + element_add_class ( + WEBKIT_DOM_ELEMENT (parent), + "-x-evo-plaintext-quoted"); + } + } + + parent = webkit_dom_node_get_parent_node (parent); + } + + return level; +} + +WebKitDOMElement * +e_html_editor_view_quote_plain_text_element (EHTMLEditorView *view, + WebKitDOMElement *element) +{ + WebKitDOMDocument *document; + WebKitDOMNode *element_clone; + WebKitDOMNodeList *list; + gint ii, length, level; + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (element)); + + element_clone = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (element), TRUE); + level = get_citation_level (WEBKIT_DOM_NODE (element), TRUE); + + /* Remove old quote characters if the exists */ + list = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (element_clone), "span.-x-evo-quoted", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), node, NULL); + } + + quote_plain_text_recursive ( + document, element_clone, element_clone, level); + + /* Replace old element with one, that is quoted */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + element_clone, + WEBKIT_DOM_NODE (element), + NULL); + + return WEBKIT_DOM_ELEMENT (element_clone); +} + +/** + * e_html_editor_view_quote_plain_text: + * @view: an #EHTMLEditorView + * + * Quote text inside citation blockquotes in plain text mode. + * + * As this function is cloning and replacing all citation blockquotes keep on + * mind that any pointers to nodes inside these blockquotes will be invalidated. + */ +WebKitDOMElement * +e_html_editor_view_quote_plain_text (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMNode *body_clone; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMNodeList *list; + WebKitDOMElement *element; + gint ii, length; + gulong attributes_length; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + /* Check if the document is already quoted */ + element = webkit_dom_document_query_selector ( + document, ".-x-evo-plaintext-quoted", NULL); + if (element) + return NULL; + + body = webkit_dom_document_get_body (document); + body_clone = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (body), TRUE); + + /* Clean unwanted spaces before and after blockquotes */ + list = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (body_clone), "blockquote[type|=cite]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *blockquote = webkit_dom_node_list_item (list, ii); + WebKitDOMNode *prev_sibling = webkit_dom_node_get_previous_sibling (blockquote); + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (blockquote); + + if (prev_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (prev_sibling), + prev_sibling, + NULL); + } + if (next_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (next_sibling), + next_sibling, + NULL); + } + if (webkit_dom_node_has_child_nodes (blockquote)) { + WebKitDOMNode *child = webkit_dom_node_get_first_child (blockquote); + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (child)) { + webkit_dom_node_remove_child ( + blockquote, + child, + NULL); + } + } + } + + quote_plain_text_recursive (document, body_clone, body_clone, 0); + + /* Copy attributes */ + attributes = webkit_dom_element_get_attributes (WEBKIT_DOM_ELEMENT (body)); + attributes_length = webkit_dom_named_node_map_get_length (attributes); + for (ii = 0; ii < attributes_length; ii++) { + gchar *name, *value; + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + name = webkit_dom_node_get_local_name (node); + value = webkit_dom_node_get_node_value (node); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body_clone), name, value, NULL); + + g_free (name); + g_free (value); + } + + /* Replace old BODY with one, that is quoted */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (body)), + body_clone, + WEBKIT_DOM_NODE (body), + NULL); + + return WEBKIT_DOM_ELEMENT (body_clone); +} + +/** + * e_html_editor_view_dequote_plain_text: + * @view: an #EHTMLEditorView + * + * Dequote already quoted plain text in editor. + * Editor have to be quoted with e_html_editor_view_quote_plain_text otherwise + * it's not working. + */ +void +e_html_editor_view_dequote_plain_text (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNodeList *paragraphs; + gint length, ii; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + paragraphs = webkit_dom_document_query_selector_all ( + document, "blockquote.-x-evo-plaintext-quoted", NULL); + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNodeList *list; + WebKitDOMElement *element; + gint jj, list_length; + + element = WEBKIT_DOM_ELEMENT (webkit_dom_node_list_item (paragraphs, ii)); + + if (is_citation_node (WEBKIT_DOM_NODE (element))) { + element_remove_class (element, "-x-evo-plaintext-quoted"); + + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-quoted", NULL); + list_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < list_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + } + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-temp-text-wrapper", NULL); + list_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < list_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + webkit_dom_node_get_first_child (node), + node, + NULL); + } + } + } +} + +/** + * e_html_editor_view_get_html_mode: + * @view: an #EHTMLEditorView + * + * Whether the editor is in HTML mode or plain text mode. In HTML mode, + * more formatting options are avilable an the email is sent as + * multipart/alternative. + * + * Returns: @TRUE when HTML mode is enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_html_mode (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->html_mode; +} + +static gint +get_indentation_level (WebKitDOMElement *element) +{ + WebKitDOMElement *parent; + gint level = 1; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (element)); + /* Count level of indentation */ + while (!WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (element_has_class (parent, "-x-evo-indented")) + level++; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (parent)); + } + + return level; +} + +static void +process_blockquote (WebKitDOMElement *blockquote) +{ + WebKitDOMNodeList *list; + int jj, length; + + /* First replace wrappers */ + list = webkit_dom_element_query_selector_all ( + blockquote, "span.-x-evo-temp-text-wrapper", NULL); + length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < length; jj++) { + WebKitDOMNode *quoted_node; + gchar *text_content, *tmp = NULL; + + quoted_node = webkit_dom_node_list_item (list, jj); + text_content = webkit_dom_node_get_text_content (quoted_node); + if (webkit_dom_node_get_previous_sibling (quoted_node)) { + tmp = g_strconcat ("<br>", text_content, NULL); + g_free (text_content); + text_content = tmp; + } + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (quoted_node), text_content, NULL); + + g_free (text_content); + } + + /* Afterwards replace quote nodes with symbols */ + list = webkit_dom_element_query_selector_all ( + blockquote, "span.-x-evo-quoted", NULL); + length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < length; jj++) { + WebKitDOMNode *quoted_node; + gchar *text_content, *tmp = NULL; + + quoted_node = webkit_dom_node_list_item (list, jj); + text_content = webkit_dom_node_get_text_content (quoted_node); + if (webkit_dom_node_get_previous_sibling (quoted_node)) { + tmp = g_strconcat ("<br>", text_content, NULL); + g_free (text_content); + text_content = tmp; + } + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (quoted_node), text_content, NULL); + + g_free (text_content); + } + + if (element_has_class (blockquote, "-x-evo-indented")) { + WebKitDOMNode *child; + gchar *spaces; + + spaces = g_strnfill (4 * get_indentation_level (blockquote), ' '); + + child = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (blockquote)); + while (child) { + /* If next sibling is indented blockqoute skip it, + * it will be processed afterwards */ + if (WEBKIT_DOM_IS_ELEMENT (child) && + element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-indented")) + child = webkit_dom_node_get_next_sibling (child); + + if (WEBKIT_DOM_IS_TEXT (child)) { + gchar *text_content; + gchar *indented_text; + + text_content = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (child)); + indented_text = g_strconcat (spaces, text_content, NULL); + + webkit_dom_text_replace_whole_text ( + WEBKIT_DOM_TEXT (child), + indented_text, + NULL); + + g_free (text_content); + g_free (indented_text); + } + + if (!child) + break; + + /* Move to next node */ + if (webkit_dom_node_has_child_nodes (child)) + child = webkit_dom_node_get_first_child (child); + else if (webkit_dom_node_get_next_sibling (child)) + child = webkit_dom_node_get_next_sibling (child); + else { + if (webkit_dom_node_is_equal_node (WEBKIT_DOM_NODE (blockquote), child)) + break; + + child = webkit_dom_node_get_parent_node (child); + if (child) + child = webkit_dom_node_get_next_sibling (child); + } + } + g_free (spaces); + + webkit_dom_element_remove_attribute (blockquote, "style"); + } +} + +/* Taken from GtkHTML */ +static gchar * +get_alpha_value (gint value, + gboolean lower) +{ + GString *str; + gchar *rv; + gint add = lower ? 'a' : 'A'; + + str = g_string_new (". "); + + do { + g_string_prepend_c (str, ((value - 1) % 26) + add); + value = (value - 1) / 26; + } while (value); + + rv = str->str; + g_string_free (str, FALSE); + + return rv; +} + +/* Taken from GtkHTML */ +static gchar * +get_roman_value (gint value, + gboolean lower) +{ + GString *str; + const gchar *base = "IVXLCDM"; + gchar *rv; + gint b, r, add = lower ? 'a' - 'A' : 0; + + if (value > 3999) + return g_strdup ("?. "); + + str = g_string_new (". "); + + for (b = 0; value > 0 && b < 7 - 1; b += 2, value /= 10) { + r = value % 10; + if (r != 0) { + if (r < 4) { + for (; r; r--) + g_string_prepend_c (str, base[b] + add); + } else if (r == 4) { + g_string_prepend_c (str, base[b + 1] + add); + g_string_prepend_c (str, base[b] + add); + } else if (r == 5) { + g_string_prepend_c (str, base[b + 1] + add); + } else if (r < 9) { + for (; r > 5; r--) + g_string_prepend_c (str, base[b] + add); + g_string_prepend_c (str, base[b + 1] + add); + } else if (r == 9) { + g_string_prepend_c (str, base[b + 2] + add); + g_string_prepend_c (str, base[b] + add); + } + } + } + + rv = str->str; + g_string_free (str, FALSE); + + return rv; +} + +static void +process_list_to_plain_text (EHTMLEditorView *view, + WebKitDOMElement *element, + gint level, + GString *output) +{ + EHTMLEditorSelectionBlockFormat format; + EHTMLEditorSelectionAlignment alignment; + gint counter = 1; + gchar *indent_per_level = g_strnfill (SPACES_PER_LIST_LEVEL, ' '); + WebKitDOMNode *item; + gint word_wrap_length = e_html_editor_selection_get_word_wrap_length ( + e_html_editor_view_get_selection (view)); + + format = e_html_editor_selection_get_list_format_from_node ( + WEBKIT_DOM_NODE (element)); + + /* Process list items to plain text */ + item = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (element)); + while (item) { + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (item)) + g_string_append (output, "\n"); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + gchar *space, *item_str = NULL; + gint ii = 0; + WebKitDOMElement *wrapped; + GString *item_value = g_string_new (""); + + alignment = e_html_editor_selection_get_list_alignment_from_node ( + WEBKIT_DOM_NODE (item)); + + wrapped = webkit_dom_element_query_selector ( + WEBKIT_DOM_ELEMENT (item), ".-x-evo-wrap-br", NULL); + /* Wrapped text */ + if (wrapped) { + WebKitDOMNode *node = webkit_dom_node_get_first_child (item); + GString *line = g_string_new (""); + while (node) { + if (WEBKIT_DOM_IS_TEXT (node)) { + /* append text from line */ + gchar *text_content; + text_content = webkit_dom_node_get_text_content (node); + g_string_append (line, text_content); + g_free (text_content); + } + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (node) && + element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-wrap-br")) { + /* put spaces before line characters -> wordwraplength - indentation */ + g_string_append (line, "\n"); + /* put spaces before line characters -> wordwraplength - indentation */ + for (ii = 0; ii < level; ii++) + g_string_append (line, indent_per_level); + g_string_append (item_value, line->str); + g_string_erase (line, 0, -1); + } + node = webkit_dom_node_get_next_sibling (node); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) + g_string_append (item_value, line->str); + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (line->str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + fill_length /= 2; + + if (fill_length < 0) + fill_length = 0; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (item_value, fill); + g_string_append (item_value, line->str); + g_free (fill); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (line->str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + + if (fill_length < 0) + fill_length = 0; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (item_value, fill); + g_string_append (item_value, line->str); + g_free (fill); + } + g_string_free (line, TRUE); + /* that same here */ + } else { + gchar *text_content = + webkit_dom_node_get_text_content (item); + g_string_append (item_value, text_content); + g_free (text_content); + } + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST) { + space = g_strnfill (SPACES_PER_LIST_LEVEL - 2, ' '); + item_str = g_strdup_printf ( + "%s* %s", space, item_value->str); + g_free (space); + } + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST) { + gint length = 1, tmp = counter; + + while ((tmp = tmp / 10) > 1) + length++; + + if (tmp == 1) + length++; + + space = g_strnfill (SPACES_PER_LIST_LEVEL - 2 - length, ' '); + item_str = g_strdup_printf ( + "%s%d. %s", space, counter, item_value->str); + g_free (space); + } + + if (format > E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST) { + gchar *value; + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA) + value = get_alpha_value (counter, FALSE); + else + value = get_roman_value (counter, FALSE); + + /* Value already containes dot and space */ + space = g_strnfill (SPACES_PER_LIST_LEVEL - strlen (value), ' '); + item_str = g_strdup_printf ( + "%s%s%s", space, value, item_value->str); + g_free (space); + g_free (value); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) { + for (ii = 0; ii < level - 1; ii++) { + g_string_append (output, indent_per_level); + } + g_string_append (output, item_str); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) { + if (!wrapped) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (item_str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + + if (fill_length < 0) + fill_length = 0; + + if (g_str_has_suffix (item_str, " ")) + fill_length++; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (output, fill); + g_free (fill); + } + if (g_str_has_suffix (item_str, " ")) + g_string_append_len (output, item_str, g_utf8_strlen (item_str, -1) - 1); + else + g_string_append (output, item_str); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) { + if (!wrapped) { + gchar *fill = NULL; + gint fill_length = 0; + + for (ii = 0; ii < level - 1; ii++) + g_string_append (output, indent_per_level); + + fill_length = word_wrap_length - g_utf8_strlen (item_str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + fill_length /= 2; + + if (fill_length < 0) + fill_length = 0; + + if (g_str_has_suffix (item_str, " ")) + fill_length++; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (output, fill); + g_free (fill); + } + if (g_str_has_suffix (item_str, " ")) + g_string_append_len (output, item_str, g_utf8_strlen (item_str, -1) - 1); + else + g_string_append (output, item_str); + } + + counter++; + item = webkit_dom_node_get_next_sibling (item); + if (item) + g_string_append (output, "\n"); + + g_free (item_str); + g_string_free (item_value, TRUE); + } else if (WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (item) || + WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (item)) { + process_list_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (item), level + 1, output); + item = webkit_dom_node_get_next_sibling (item); + } else { + item = webkit_dom_node_get_next_sibling (item); + } + } + + if (webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (element))) + g_string_append (output, "\n"); + + g_free (indent_per_level); +} + +static void +remove_base_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "class"); + webkit_dom_element_remove_attribute (element, "id"); + webkit_dom_element_remove_attribute (element, "name"); +} + +static void +remove_evolution_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "x-evo-smiley"); + webkit_dom_element_remove_attribute (element, "data-converted"); + webkit_dom_element_remove_attribute (element, "data-edit-as-new"); + webkit_dom_element_remove_attribute (element, "data-inline"); + webkit_dom_element_remove_attribute (element, "data-uri"); + webkit_dom_element_remove_attribute (element, "data-message"); + webkit_dom_element_remove_attribute (element, "data-name"); + webkit_dom_element_remove_attribute (element, "data-new-message"); + webkit_dom_element_remove_attribute (element, "spellcheck"); +} +/* +static void +remove_style_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "bgcolor"); + webkit_dom_element_remove_attribute (element, "background"); + webkit_dom_element_remove_attribute (element, "style"); +} +*/ +static void +process_elements (EHTMLEditorView *view, + WebKitDOMNode *node, + gboolean to_html, + gboolean changing_mode, + gboolean to_plain_text, + GString *buffer) +{ + WebKitDOMNodeList *nodes; + gulong ii, length; + gchar *content; + gboolean skip_nl = FALSE; + + if (to_plain_text && !buffer) + return; + + if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (node)) { + if (changing_mode && to_plain_text) { + WebKitDOMNamedNodeMap *attributes; + gulong attributes_length; + + /* Copy attributes */ + g_string_append (buffer, "<html><head></head><body "); + attributes = webkit_dom_element_get_attributes ( + WEBKIT_DOM_ELEMENT (node)); + attributes_length = + webkit_dom_named_node_map_get_length (attributes); + + for (ii = 0; ii < attributes_length; ii++) { + gchar *name, *value; + WebKitDOMNode *node = + webkit_dom_named_node_map_item ( + attributes, ii); + + name = webkit_dom_node_get_local_name (node); + value = webkit_dom_node_get_node_value (node); + + g_string_append (buffer, name); + g_string_append (buffer, "=\""); + g_string_append (buffer, value); + g_string_append (buffer, "\" "); + + g_free (name); + g_free (value); + } + g_string_append (buffer, ">"); + } + if (to_html) + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (node)); + } + + nodes = webkit_dom_node_get_child_nodes (node); + length = webkit_dom_node_list_get_length (nodes); + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *child; + gboolean skip_node = FALSE; + + child = webkit_dom_node_list_item (nodes, ii); + + if (WEBKIT_DOM_IS_TEXT (child)) { + gchar *content, *tmp; + GRegex *regex; + + content = webkit_dom_node_get_text_content (child); + /* Replace tabs with 4 whitespaces, otherwise they got + * replaced by single whitespace */ + if (strstr (content, "\x9")) { + regex = g_regex_new ("\x9", 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, " ", 0, NULL); + webkit_dom_node_set_text_content (child, tmp, NULL); + g_free (content); + g_free (tmp); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + + if (strstr (content, UNICODE_ZERO_WIDTH_SPACE)) { + regex = g_regex_new (UNICODE_ZERO_WIDTH_SPACE, 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, "", 0, NULL); + webkit_dom_node_set_text_content (child, tmp, NULL); + g_free (tmp); + g_free (content); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + + if (to_plain_text && !changing_mode) { + gchar *style; + const gchar *css_align; + + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + gchar *align; + gchar *content_with_align; + gint length; + gint word_wrap_length = + e_html_editor_selection_get_word_wrap_length ( + e_html_editor_view_get_selection (view)); + + if (!g_str_has_prefix (css_align + 12, "left")) { + if (g_str_has_prefix (css_align + 12, "center")) + length = (word_wrap_length - g_utf8_strlen (content, -1)) / 2; + else + length = word_wrap_length - g_utf8_strlen (content, -1); + + if (g_str_has_suffix (content, " ")) { + char *tmp; + + length++; + align = g_strnfill (length, ' '); + + tmp = g_strndup (content, g_utf8_strlen (content, -1) -1); + + content_with_align = g_strconcat ( + align, tmp, NULL); + g_free (tmp); + } else { + align = g_strnfill (length, ' '); + + content_with_align = g_strconcat ( + align, content, NULL); + } + + g_free (content); + g_free (align); + content = content_with_align; + } + } + + g_free (style); + } + + if (to_plain_text || changing_mode) + g_string_append (buffer, content); + + g_free (content); + + goto next; + } + + if (WEBKIT_DOM_IS_COMMENT (child) || !WEBKIT_DOM_IS_ELEMENT (child)) + goto next; + + /* Leave caret position untouched */ + if (element_has_id (WEBKIT_DOM_ELEMENT (child), "-x-evo-caret-position")) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + } + if (to_html) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (child), + child, + NULL); + } + + skip_node = TRUE; + } + + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "Apple-tab-span")) { + if (!changing_mode && to_plain_text) { + gchar *content, *tmp; + GRegex *regex; + + content = webkit_dom_node_get_text_content (child); + /* Replace tabs with 4 whitespaces, otherwise they got + * replaced by single whitespace */ + if (strstr (content, "\x9")) { + regex = g_regex_new ("\x9", 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, " ", 0, NULL); + g_string_append (buffer, tmp); + g_free (tmp); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + } + if (to_html) { + element_remove_class ( + WEBKIT_DOM_ELEMENT (child), + "Applet-tab-span"); + } + + skip_node = TRUE; + } + + /* Leave blockquotes as they are */ + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (child)) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } else { + if (!changing_mode && to_plain_text) { + if (get_citation_level (child, FALSE) == 0) { + gchar *value; + value = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (child), "type"); + if (value && g_strcmp0 (value, "cite") == 0) { + g_string_append (buffer, "\n"); + } + g_free (value); + } + } + process_blockquote (WEBKIT_DOM_ELEMENT (child)); + } + } + + if (WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (child) || + WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (child)) { + if (to_plain_text) { + if (changing_mode) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + } else { + process_list_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (child), 1, buffer); + } + skip_node = TRUE; + goto next; + } + } + + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-resizable-wrapper") && + !element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-smiley-wrapper")) { + WebKitDOMNode *image = + webkit_dom_node_get_first_child (child); + + if (to_html && WEBKIT_DOM_IS_HTML_IMAGE_ELEMENT (image)) { + remove_evolution_attributes ( + WEBKIT_DOM_ELEMENT (image)); + + webkit_dom_node_replace_child ( + node, image, child, NULL); + } + + skip_node = TRUE; + } + + /* Leave paragraphs as they are */ + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-paragraph")) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) { + remove_base_attributes (WEBKIT_DOM_ELEMENT (child)); + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (child)); + } + } + + /* Signature */ + if (WEBKIT_DOM_IS_HTML_DIV_ELEMENT (child)) { + WebKitDOMNode *first_child; + + first_child = webkit_dom_node_get_first_child (child); + if (WEBKIT_DOM_IS_ELEMENT (first_child)) { + if (element_has_class ( + WEBKIT_DOM_ELEMENT (first_child), + "-x-evo-signature")) { + + if (to_html) { + remove_base_attributes ( + WEBKIT_DOM_ELEMENT (first_child)); + remove_evolution_attributes ( + WEBKIT_DOM_ELEMENT (first_child)); + } + if (to_plain_text && !changing_mode) { + g_string_append (buffer, "\n"); + content = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (first_child)); + g_string_append (buffer, content); + g_free (content); + skip_nl = TRUE; + } + skip_node = TRUE; + } + } + } + + /* Replace smileys with their text representation */ + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-smiley-wrapper")) { + if (to_plain_text && !changing_mode) { + WebKitDOMNode *text_version; + + text_version = webkit_dom_node_get_last_child (child); + content = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (text_version)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) { + WebKitDOMElement *img; + + img = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_first_child (child)), + + remove_evolution_attributes (img); + remove_base_attributes (img); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (child), + WEBKIT_DOM_NODE (img), + child, + NULL); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (child), + child, + NULL); + skip_node = TRUE; + } + } + + /* Leave PRE elements untouched */ + if (WEBKIT_DOM_IS_HTML_PRE_ELEMENT (child)) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (child)); + } + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (child)) { + if (to_plain_text) { + /* Insert new line when we hit BR element */ + g_string_append (buffer, changing_mode ? "<br>" : "\n"); + } + } + next: + if (webkit_dom_node_has_child_nodes (child) && !skip_node) + process_elements ( + view, child, to_html, changing_mode, to_plain_text, buffer); + } + + if (to_plain_text && ( + WEBKIT_DOM_IS_HTML_DIV_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_PARAGRAPH_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_PRE_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (node))) { + + gboolean add_br = TRUE; + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (node); + + /* If we don't have next sibling (last element in body) or next element is + * signature we are not adding the BR element */ + if (!next_sibling) + add_br = FALSE; + + if (element_has_class (webkit_dom_node_get_parent_element (node), "-x-evo-indented")) + add_br = FALSE; + + if (next_sibling && WEBKIT_DOM_IS_HTML_DIV_ELEMENT (next_sibling)) { + if (webkit_dom_element_query_selector ( + WEBKIT_DOM_ELEMENT (next_sibling), + "span.-x-evo-signature", NULL)) { + + add_br = FALSE; + } + } + + content = webkit_dom_node_get_text_content (node); + if (add_br && g_utf8_strlen (content, -1) > 0 && !skip_nl) + g_string_append (buffer, changing_mode ? "<br>" : "\n"); + + g_free (content); + } + +} + +static void +remove_wrapping (EHTMLEditorView *view) +{ + gint length; + gint ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + list = webkit_dom_document_query_selector_all (document, "br.-x-evo-wrap-br", NULL); + + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), node, NULL); + } +} + +static void +remove_images_in_element (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean html_mode) +{ + gint length, ii; + WebKitDOMNodeList *images; + + images = webkit_dom_element_query_selector_all ( + element, "img:not(.-x-evo-smiley-img)", NULL); + + length = webkit_dom_node_list_get_length (images); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *img = webkit_dom_node_list_item (images, ii); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (img), img, NULL); + } +} + +static void +remove_images (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + remove_images_in_element ( + view, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document)), + view->priv->html_mode); +} + +static void +toggle_smileys (EHTMLEditorView *view) +{ + gboolean html_mode; + gint length; + gint ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *smileys; + + html_mode = e_html_editor_view_get_html_mode (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + smileys = webkit_dom_document_query_selector_all ( + document, "img.-x-evo-smiley-img", NULL); + + length = webkit_dom_node_list_get_length (smileys); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *img = webkit_dom_node_list_item (smileys, ii); + WebKitDOMNode *text = webkit_dom_node_get_next_sibling (img); + WebKitDOMElement *parent = webkit_dom_node_get_parent_element (img); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (html_mode ? text : img), + "style", + "display: none", + NULL); + + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (html_mode ? img : text), "style"); + + if (html_mode) + element_add_class (parent, "-x-evo-resizable-wrapper"); + else + element_remove_class (parent, "-x-evo-resizable-wrapper"); + } +} + +static void +toggle_paragraphs_style_in_element (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean html_mode) +{ + EHTMLEditorSelection *selection; + gint ii, length; + WebKitDOMNodeList *paragraphs; + + html_mode = e_html_editor_view_get_html_mode (view); + selection = e_html_editor_view_get_selection (view); + + paragraphs = webkit_dom_element_query_selector_all ( + element, ".-x-evo-paragraph", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + + for (ii = 0; ii < length; ii++) { + gchar *style; + const gchar *css_align; + WebKitDOMNode *node = webkit_dom_node_list_item (paragraphs, ii); + + if (html_mode) { + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), + "style", + g_str_has_prefix (css_align + 12, "center") ? + "text-align: center" : + "text-align: right", + NULL); + } else { + /* In HTML mode the paragraphs don't have width limit */ + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + } + g_free (style); + } else { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node (node); + /* If the paragraph is inside indented paragraph don't set + * the style as it will be inherited */ + if (!element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-indented")) { + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + const gchar *style_to_add; + + style_to_add = g_str_has_prefix ( + css_align + 12, "center") ? + "text-align: center;" : + "text-align: right;"; + + /* In HTML mode the paragraphs have width limit */ + e_html_editor_selection_set_paragraph_style ( + selection, WEBKIT_DOM_ELEMENT (node), + -1, 0, style_to_add); + } + g_free (style); + } + } + } +} + +static void +toggle_paragraphs_style (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + toggle_paragraphs_style_in_element ( + view, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document)), + view->priv->html_mode); +} + +static gchar * +process_content_for_saving_as_draft (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMElement *element, *document_element; + gchar *content; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + element = webkit_dom_document_get_element_by_id ( + document, "-x-evo-caret-position"); + + if (element) + webkit_dom_element_set_attribute ( + element, "style", "display: none; color: red;", NULL); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-evo-draft", "", NULL); + + document_element = webkit_dom_document_get_document_element (document); + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (document_element)); + + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-evo-draft"); + + if (element) + webkit_dom_element_set_attribute ( + element, "style", "color: red;", NULL); + + return content; +} + +static gchar * +process_content_for_mode_change (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNode *body; + GString *plain_text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + + plain_text = g_string_sized_new (1024); + + process_elements (view, body, FALSE, TRUE, TRUE, plain_text); + + g_string_append (plain_text, "</body></html>"); + + return g_string_free (plain_text, FALSE); +} + +static void +convert_element_from_html_to_plain_text (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean *wrap, + gboolean *quote) +{ + EHTMLEditorSelection *selection; + gint blockquotes_count; + gchar *inner_text, *inner_html; + gboolean restore = TRUE; + WebKitDOMDocument *document; + WebKitDOMElement *top_signature, *signature, *blockquote, *main_blockquote; + WebKitDOMNode *signature_clone, *from; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (element)); + + top_signature = webkit_dom_element_query_selector ( + element, ".-x-evo-top-signature", NULL); + signature = webkit_dom_element_query_selector ( + element, "span.-x-evo-signature", NULL); + main_blockquote = webkit_dom_element_query_selector ( + element, "#-x-evo-main-cite", NULL); + + blockquote = webkit_dom_document_create_element ( + document, "blockquote", NULL); + + if (main_blockquote) { + WebKitDOMElement *input_start; + + webkit_dom_element_set_attribute ( + blockquote, "type", "cite", NULL); + + input_start = webkit_dom_element_query_selector ( + element, "#-x-evo-input-start", NULL); + + restore = input_start ? TRUE : FALSE; + + if (input_start) { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (input_start), + e_html_editor_selection_get_caret_position_node ( + document), + NULL); + } + from = WEBKIT_DOM_NODE (main_blockquote); + } else { + if (signature) { + signature_clone = webkit_dom_node_clone_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + TRUE); + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (element), + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + NULL); + } + from = WEBKIT_DOM_NODE (element); + } + + blockquotes_count = create_text_markers_for_citations_in_element ( + WEBKIT_DOM_ELEMENT (from)); + + inner_text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (from)); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), inner_text, NULL); + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote)); + + parse_html_into_paragraphs ( + view, document, + main_blockquote ? blockquote : WEBKIT_DOM_ELEMENT (element), + inner_html, + FALSE); + + if (main_blockquote) { + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (main_blockquote)), + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (main_blockquote), + NULL); + + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (element)); + *wrap = TRUE; + } else { + WebKitDOMNode *first_child; + + if (signature) { + if (!top_signature) { + signature_clone = webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + signature_clone, + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (element), + signature_clone, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)), + NULL); + } + } + + first_child = webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)); + + if (!webkit_dom_node_has_child_nodes (first_child)) { + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (first_child), + UNICODE_ZERO_WIDTH_SPACE, + NULL); + } + webkit_dom_node_insert_before ( + first_child, + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_first_child (first_child), + NULL); + + *wrap = TRUE; + } + + *quote = main_blockquote || blockquotes_count > 0; + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-converted", "", NULL); + + g_free (inner_text); + g_free (inner_html); + + if (restore) + e_html_editor_selection_restore_caret_position (selection); +} + +static gchar * +process_content_for_plain_text (EHTMLEditorView *view) +{ + gboolean converted, wrap = FALSE, quote = FALSE, clean = FALSE; + gint length, ii; + GString *plain_text; + WebKitDOMDocument *document; + WebKitDOMNode *body, *source; + WebKitDOMNodeList *paragraphs; + + plain_text = g_string_sized_new (1024); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + converted = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted"); + source = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (body), TRUE); + + /* If composer is in HTML mode we have to move the content to plain version */ + if (view->priv->html_mode) { + if (converted) { + toggle_paragraphs_style_in_element ( + view, WEBKIT_DOM_ELEMENT (source), FALSE); + remove_images_in_element ( + view, WEBKIT_DOM_ELEMENT (source), FALSE); + } else { + gchar *inner_html; + WebKitDOMElement *div; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (body)); + + div = webkit_dom_document_create_element ( + document, "div", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (div), inner_html, NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (div), + NULL); + + paragraphs = webkit_dom_element_query_selector_all ( + div, "#-x-evo-input-start", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *paragraph; + + paragraph = webkit_dom_node_list_item (paragraphs, ii); + + webkit_dom_element_set_id ( + WEBKIT_DOM_ELEMENT (paragraph), ""); + } + + convert_element_from_html_to_plain_text ( + view, div, &wrap, "e); + + source = WEBKIT_DOM_NODE (div); + + clean = TRUE; + } + } + + paragraphs = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (source), ".-x-evo-paragraph", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *paragraph; + + paragraph = webkit_dom_node_list_item (paragraphs, ii); + + if (WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (paragraph) || + WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (paragraph)) { + WebKitDOMNode *item = webkit_dom_node_get_first_child (paragraph); + + while (item) { + WebKitDOMNode *next_item = + webkit_dom_node_get_next_sibling (item); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + e_html_editor_selection_wrap_paragraph ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (item)); + } + item = next_item; + } + } else { + e_html_editor_selection_wrap_paragraph ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (paragraph)); + } + } + + paragraphs = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (source), + "span[id^=\"-x-evo-selection-\"], span#-x-evo-caret-position", + NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (paragraphs, ii); + WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node); + + webkit_dom_node_remove_child (parent, node, NULL); + webkit_dom_node_normalize (parent); + } + + if (view->priv->html_mode || quote) + quote_plain_text_recursive (document, source, source, 0); + + process_elements (view, source, FALSE, FALSE, TRUE, plain_text); + + if (clean) + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), source, NULL); + + /* Return text content between <body> and </body> */ + return g_string_free (plain_text, FALSE); +} + +static gchar * +process_content_for_html (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNode *body; + WebKitDOMElement *element; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + process_elements (view, body, TRUE, FALSE, FALSE, NULL); + element = webkit_dom_document_get_document_element (document); + return webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); +} + +static gboolean +show_lose_formatting_dialog (EHTMLEditorView *view) +{ + gint result; + GtkWidget *toplevel, *dialog; + GtkWindow *parent = NULL; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + + if (GTK_IS_WINDOW (toplevel)) + parent = GTK_WINDOW (toplevel); + + dialog = gtk_message_dialog_new ( + parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_NONE, + _("Turning HTML mode off will cause the text " + "to lose all formatting. Do you want to continue?")); + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + _("_Don't lose formatting"), GTK_RESPONSE_CANCEL, + _("_Lose formatting"), GTK_RESPONSE_OK, + NULL); + + result = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (result != GTK_RESPONSE_OK) { + gtk_widget_destroy (dialog); + /* Nothing has changed, but notify anyway */ + g_object_notify (G_OBJECT (view), "html-mode"); + return FALSE; + } + + gtk_widget_destroy (dialog); + + return TRUE; +} + +static void +clear_attributes (WebKitDOMDocument *document) +{ + gint length, ii; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + WebKitDOMHTMLHeadElement *head = webkit_dom_document_get_head (document); + WebKitDOMElement *document_element = + webkit_dom_document_get_document_element (document); + + /* Remove all attributes from HTML element */ + attributes = webkit_dom_element_get_attributes (document_element); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = length - 1; ii >= 0; ii--) { + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + webkit_dom_element_remove_attribute_node ( + document_element, WEBKIT_DOM_ATTR (node), NULL); + } + + /* Remove everything from HEAD element */ + while (webkit_dom_node_has_child_nodes (WEBKIT_DOM_NODE (head))) { + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (head), + webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (head)), + NULL); + } + + /* Remove non Evolution attributes from BODY element */ + attributes = webkit_dom_element_get_attributes (WEBKIT_DOM_ELEMENT (body)); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = length - 1; ii >= 0; ii--) { + gchar *name; + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + name = webkit_dom_node_get_local_name (node); + + if (!g_str_has_prefix (name, "data-") || + g_str_has_prefix (name, "data-inline") || + g_str_has_prefix (name, "data-name")) { + webkit_dom_element_remove_attribute_node ( + WEBKIT_DOM_ELEMENT (body), + WEBKIT_DOM_ATTR (node), + NULL); + } + + g_free (name); + } +} + +static void +convert_when_changing_composer_mode (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gboolean quote = FALSE, wrap = FALSE; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + convert_element_from_html_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (body), &wrap, "e); + + if (wrap) + e_html_editor_selection_wrap_paragraphs_in_document (selection, document); + + if (quote) { + e_html_editor_selection_save_caret_position (selection); + body = WEBKIT_DOM_HTML_ELEMENT (e_html_editor_view_quote_plain_text (view)); + e_html_editor_selection_restore_caret_position (selection); + } + + clear_attributes (document); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted", "", NULL); + + /* Update fonts - in plain text we only want monospace */ + e_html_editor_view_update_fonts (view); + + e_html_editor_view_force_spell_check (view); +} + +/** + * e_html_editor_view_set_html_mode: + * @view: an #EHTMLEditorView + * @html_mode: @TRUE to enable HTML mode, @FALSE to enable plain text mode + * + * When switching from HTML to plain text mode, user will be prompted whether + * he/she really wants to switch the mode and lose all formatting. When user + * declines, the property is not changed. When they accept, the all formatting + * is lost. + */ +void +e_html_editor_view_set_html_mode (EHTMLEditorView *view, + gboolean html_mode) +{ + EHTMLEditorSelection *selection; + gboolean is_from_new_message, converted, edit_as_new, message, convert; + gboolean reply, hide; + WebKitDOMElement *blockquote; + WebKitDOMHTMLElement *body; + WebKitDOMDocument *document; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + is_from_new_message = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-new-message"); + converted = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted"); + edit_as_new = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-edit-as-new"); + message = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-message"); + + reply = !is_from_new_message && !edit_as_new && message; + hide = !reply && !converted; + + convert = message && ((!hide && reply && !converted) || (edit_as_new && !converted)); + + /* If toggling from HTML to plain text mode, ask user first */ + if (convert && view->priv->html_mode && !html_mode) { + if (!show_lose_formatting_dialog (view)) + return; + + view->priv->html_mode = html_mode; + + convert_when_changing_composer_mode (view); + + goto out; + } + + if (html_mode == view->priv->html_mode) + return; + + view->priv->html_mode = html_mode; + + /* Update fonts - in plain text we only want monospace */ + e_html_editor_view_update_fonts (view); + + blockquote = webkit_dom_document_query_selector ( + document, "blockquote[type|=cite]", NULL); + + if (view->priv->html_mode) { + if (blockquote) + e_html_editor_view_dequote_plain_text (view); + + toggle_paragraphs_style (view); + toggle_smileys (view); + remove_wrapping (view); + } else { + gchar *plain; + + /* Save caret position -> it will be restored in e-composer-private.c */ + e_html_editor_selection_save_caret_position (selection); + + if (blockquote) { + e_html_editor_selection_wrap_paragraphs_in_document ( + selection, document); + e_html_editor_view_quote_plain_text (view); + } + + toggle_paragraphs_style (view); + toggle_smileys (view); + remove_images (view); + + plain = process_content_for_mode_change (view); + + if (*plain) + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), plain, NULL, NULL, "file://"); + + g_free (plain); + } + + out: + g_object_notify (G_OBJECT (view), "html-mode"); +} + +/** + * e_html_editor_view_get_inline_spelling: + * @view: an #EHTMLEditorView + * + * Returns whether automatic spellchecking is enabled or not. When enabled, + * editor will perform spellchecking as user is typing. Otherwise spellcheck + * has to be run manually from menu. + * + * Returns: @TRUE when automatic spellchecking is enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_inline_spelling (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->inline_spelling; +} + +/** + * e_html_editor_view_set_inline_spelling: + * @view: an #EHTMLEditorView + * @inline_spelling: @TRUE to enable automatic spellchecking, @FALSE otherwise + * + * Enables or disables automatic spellchecking. + */ +void +e_html_editor_view_set_inline_spelling (EHTMLEditorView *view, + gboolean inline_spelling) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->inline_spelling == inline_spelling) + return; + + view->priv->inline_spelling = inline_spelling; + + g_object_notify (G_OBJECT (view), "inline-spelling"); +} + +/** + * e_html_editor_view_get_magic_links: + * @view: an #EHTMLEditorView + * + * Returns whether automatic links conversion is enabled. When enabled, the editor + * will automatically convert any HTTP links into clickable HTML links. + * + * Returns: @TRUE when magic links are enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_magic_links (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->magic_links; +} + +/** + * e_html_editor_view_set_magic_links: + * @view: an #EHTMLEditorView + * @magic_links: @TRUE to enable magic links, @FALSE to disable them + * + * Enables or disables automatic links conversion. + */ +void +e_html_editor_view_set_magic_links (EHTMLEditorView *view, + gboolean magic_links) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->magic_links == magic_links) + return; + + view->priv->magic_links = magic_links; + + g_object_notify (G_OBJECT (view), "magic-links"); +} + +/** + * e_html_editor_view_get_magic_smileys: + * @view: an #EHTMLEditorView + * + * Returns whether automatic conversion of smileys is enabled or disabled. When + * enabled, the editor will automatically convert text smileys ( :-), ;-),...) + * into images. + * + * Returns: @TRUE when magic smileys are enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_magic_smileys (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->magic_smileys; +} + +/** + * e_html_editor_view_set_magic_smileys: + * @view: an #EHTMLEditorView + * @magic_smileys: @TRUE to enable magic smileys, @FALSE to disable them + * + * Enables or disables magic smileys. + */ +void +e_html_editor_view_set_magic_smileys (EHTMLEditorView *view, + gboolean magic_smileys) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->magic_smileys == magic_smileys) + return; + + view->priv->magic_smileys = magic_smileys; + + g_object_notify (G_OBJECT (view), "magic-smileys"); +} + +/** + * e_html_editor_view_get_spell_checker: + * @view: an #EHTMLEditorView + * + * Returns an #ESpellChecker object that is used to perform spellchecking. + * + * Returns: An always-valid #ESpellChecker object + */ +ESpellChecker * +e_html_editor_view_get_spell_checker (EHTMLEditorView *view) +{ + return E_SPELL_CHECKER (webkit_get_text_checker ()); +} + +/** + * e_html_editor_view_get_text_html: + * @view: an #EHTMLEditorView: + * + * Returns processed HTML content of the editor document (with elements attributes + * used in Evolution composer) + * + * Returns: A newly allocated string + */ +gchar * +e_html_editor_view_get_text_html (EHTMLEditorView *view) +{ + return process_content_for_html (view); +} + +/** + * e_html_editor_view_get_text_html_for_drafts: + * @view: an #EHTMLEditorView: + * + * Returns HTML content of the editor document (without elements attributes + * used in Evolution composer) + * + * Returns: A newly allocated string + */ +gchar * +e_html_editor_view_get_text_html_for_drafts (EHTMLEditorView *view) +{ + return process_content_for_saving_as_draft (view); +} + +/** + * e_html_editor_view_get_text_plain: + * @view: an #EHTMLEditorView + * + * Returns plain text content of the @view. The algorithm removes any + * formatting or styles from the document and keeps only the text and line + * breaks. + * + * Returns: A newly allocated string with plain text content of the document. + */ +gchar * +e_html_editor_view_get_text_plain (EHTMLEditorView *view) +{ + return process_content_for_plain_text (view); +} + +static void +convert_and_load_html_to_plain_text (EHTMLEditorView *view, + const gchar *html) +{ + view->priv->convertor_insert = FALSE; + + webkit_web_view_load_string ( + view->priv->convertor_web_view, html, NULL, NULL, "file://"); +} + +void +e_html_editor_view_convert_and_insert_html_to_plain_text (EHTMLEditorView *view, + const gchar *html) +{ + view->priv->convertor_insert = TRUE; + + webkit_web_view_load_string ( + view->priv->convertor_web_view, html, NULL, NULL, "file://"); +} + +/** + * e_html_editor_view_set_text_html: + * @view: an #EHTMLEditorView + * @text: HTML code to load into the editor + * + * Loads given @text into the editor, destroying any content already present. + */ +void +e_html_editor_view_set_text_html (EHTMLEditorView *view, + const gchar *text) +{ + view->priv->reload_in_progress = TRUE; + + /* Only convert messages that are in HTML */ + if (!view->priv->html_mode && *text && !strstr (text, "data-evo-draft")) { + if (strstr (text, "<!-- text/html -->")) { + if (!show_lose_formatting_dialog (view)) { + e_html_editor_view_set_html_mode (view, TRUE); + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, NULL, NULL, "file://"); + return; + } + convert_and_load_html_to_plain_text (view, text); + } else { + convert_and_load_html_to_plain_text (view, text); + } + } else { + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, NULL, NULL, "file://"); + } +} + +/** + * e_html_editor_view_set_text_plain: + * @view: an #EHTMLEditorView + * @text: A plain text to load into the editor + * + * Loads given @text into the editor, destryoing any content already present. + */ +void +e_html_editor_view_set_text_plain (EHTMLEditorView *view, + const gchar *text) +{ + view->priv->reload_in_progress = TRUE; + + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, "text/plain", NULL, "file://"); +} + +/** + * e_html_editor_view_paste_clipboard_quoted: + * @view: an #EHTMLEditorView + * + * Pastes current content of clipboard into the editor as quoted text + */ +void +e_html_editor_view_paste_clipboard_quoted (EHTMLEditorView *view) +{ + EHTMLEditorViewClass *class; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + class = E_HTML_EDITOR_VIEW_GET_CLASS (view); + g_return_if_fail (class->paste_clipboard_quoted != NULL); + + class->paste_clipboard_quoted (view); +} + +void +e_html_editor_view_embed_styles (EHTMLEditorView *view) +{ + WebKitWebSettings *settings; + WebKitDOMDocument *document; + WebKitDOMElement *sheet; + gchar *stylesheet_uri; + gchar *stylesheet_content; + const gchar *stylesheet; + gsize length; + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + g_object_get ( + G_OBJECT (settings), + "user-stylesheet-uri", &stylesheet_uri, + NULL); + + stylesheet = strstr (stylesheet_uri, ","); + stylesheet_content = (gchar *) g_base64_decode (stylesheet, &length); + g_free (stylesheet_uri); + + if (length == 0) { + g_free (stylesheet_content); + return; + } + + e_web_view_create_and_add_css_style_sheet (document, "-x-evo-composer-sheet"); + + sheet = webkit_dom_document_get_element_by_id (document, "-x-evo-composer-sheet"); + webkit_dom_element_set_attribute ( + sheet, + "type", + "text/css", + NULL); + + webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (sheet), stylesheet_content, NULL); + + g_free (stylesheet_content); +} + +void +e_html_editor_view_remove_embed_styles (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMElement *sheet; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + sheet = webkit_dom_document_get_element_by_id ( + document, "-x-evo-composer-sheet"); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (sheet)), + WEBKIT_DOM_NODE (sheet), + NULL); +} + +static const gchar * +citation_color_level_1 (void) +{ + return "rgb(114,159,207)"; /* Sky Blue 1 */ +} + +static const gchar * +citation_color_level_2 (void) +{ + return "rgb(173,127,168)"; /* Plum 1 */ +} + +static const gchar * +citation_color_level_3 (void) +{ + return "rgb(138,226,52)"; /* Chameleon 1 */ +} + +static const gchar * +citation_color_level_4 (void) +{ + return "rgb(252,175,62)"; /* Orange 1 */ +} + +static const gchar * +citation_color_level_5 (void) +{ + return "rgb(233,185,110)"; /* Chocolate 1 */ +} + +/** + * e_html_editor_view_update_fonts: + * @view: an #EHTMLEditorView + * + * Forces the editor to reload font settings from WebKitWebSettings and apply + * it on the content of the editor document. + */ +void +e_html_editor_view_update_fonts (EHTMLEditorView *view) +{ + GString *stylesheet; + gchar *base64; + gchar *aa = NULL; + WebKitWebSettings *settings; + PangoFontDescription *ms, *vw; + const gchar *styles[] = { "normal", "oblique", "italic" }; + const gchar *smoothing = NULL; + GtkStyleContext *context; + GdkColor *link = NULL; + GdkColor *visited = NULL; + gchar *font; + + font = g_settings_get_string ( + view->priv->font_settings, + "monospace-font-name"); + ms = pango_font_description_from_string ( + font ? font : "monospace 10"); + g_free (font); + + if (view->priv->html_mode) { + font = g_settings_get_string ( + view->priv->font_settings, + "font-name"); + vw = pango_font_description_from_string ( + font ? font : "serif 10"); + g_free (font); + } else { + /* When in plain text mode, force monospace font */ + vw = pango_font_description_copy (ms); + } + + stylesheet = g_string_new (""); + g_string_append_printf ( + stylesheet, + "body {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n", + pango_font_description_get_family (vw), + pango_font_description_get_size (vw) / PANGO_SCALE, + pango_font_description_get_weight (vw), + styles[pango_font_description_get_style (vw)]); + + if (view->priv->aliasing_settings != NULL) + aa = g_settings_get_string ( + view->priv->aliasing_settings, "antialiasing"); + + if (g_strcmp0 (aa, "none") == 0) + smoothing = "none"; + else if (g_strcmp0 (aa, "grayscale") == 0) + smoothing = "antialiased"; + else if (g_strcmp0 (aa, "rgba") == 0) + smoothing = "subpixel-antialiased"; + + if (smoothing != NULL) + g_string_append_printf ( + stylesheet, + " -webkit-font-smoothing: %s;\n", + smoothing); + + g_free (aa); + + g_string_append (stylesheet, "}\n"); + + g_string_append_printf ( + stylesheet, + "pre,code,.pre {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n" + "}", + pango_font_description_get_family (ms), + pango_font_description_get_size (ms) / PANGO_SCALE, + pango_font_description_get_weight (ms), + styles[pango_font_description_get_style (ms)]); + + context = gtk_widget_get_style_context (GTK_WIDGET (view)); + gtk_style_context_get_style ( + context, + "link-color", &link, + "visited-link-color", &visited, + NULL); + + if (link == NULL) { + link = g_slice_new0 (GdkColor); + link->blue = G_MAXINT16; + } + + if (visited == NULL) { + visited = g_slice_new0 (GdkColor); + visited->red = G_MAXINT16; + } + + g_string_append_printf ( + stylesheet, + "a {\n" + " color: #%06x;\n" + "}\n" + "a:visited {\n" + " color: #%06x;\n" + "}\n", + e_color_to_value (link), + e_color_to_value (visited)); + + /* See bug #689777 for details */ + g_string_append ( + stylesheet, + "p,pre,code,address {\n" + " margin: 0;\n" + "}\n" + "h1,h2,h3,h4,h5,h6 {\n" + " margin-top: 0.2em;\n" + " margin-bottom: 0.2em;\n" + "}\n"); + + g_string_append ( + stylesheet, + "img " + "{\n" + " height: inherit; \n" + " width: inherit; \n" + "}\n"); + + g_string_append ( + stylesheet, + "span.-x-evo-resizable-wrapper " + "{\n" + " resize: both; \n" + " overflow: hidden; \n" + " display: inline-block; \n" + "}\n"); + + g_string_append ( + stylesheet, + "span.-x-evo-resizable-wrapper:hover " + "{\n" + " outline: 1px dashed red; \n" + "}\n"); + + g_string_append ( + stylesheet, + "ul,ol " + "{\n" + " -webkit-padding-start: 7ch; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-alignt-left " + "{\n" + " text-align: left; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-align-center " + "{\n" + " text-align: center; \n" + " -webkit-padding-start: 0ch; \n" + " margin-left: -3ch; \n" + " margin-right: 1ch; \n" + " list-style-position: inside; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-align-right " + "{\n" + " text-align: right; \n" + " -webkit-padding-start: 0ch; \n" + " margin-left: -3ch; \n" + " margin-right: 1ch; \n" + " list-style-position: inside; \n" + "}\n"); + + g_string_append ( + stylesheet, + "ol,ul " + "{\n" + " -webkit-margin-before: 0em; \n" + " -webkit-margin-after: 0em; \n" + "}\n"); + + g_string_append ( + stylesheet, + "blockquote " + "{\n" + " -webkit-margin-before: 0em; \n" + " -webkit-margin-after: 0em; \n" + "}\n"); + + g_string_append ( + stylesheet, + "blockquote[type=cite] " + "{\n" + " padding: 0.0ex 0ex;\n" + " margin: 0ex;\n" + " -webkit-margin-start: 0em; \n" + " -webkit-margin-end : 0em; \n" + " color: #737373 !important;\n" + "}\n"); + + g_string_append_printf ( + stylesheet, + ".quote_character " + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_1 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_2 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_3 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_4 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_5 ()); + + g_string_append ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " padding: 0.4ex 1ex;\n" + " margin: 1ex;\n" + " border-width: 0px 2px 0px 2px;\n" + " border-style: none solid none solid;\n" + " border-radius: 2px;\n" + "}\n"); + + /* Block quote border colors are borrowed from Thunderbird. */ + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_1 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_2 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_3 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_4 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_5 ()); + + gdk_color_free (link); + gdk_color_free (visited); + + base64 = g_base64_encode ((guchar *) stylesheet->str, stylesheet->len); + g_string_free (stylesheet, TRUE); + + stylesheet = g_string_new ("data:text/css;charset=utf-8;base64,"); + g_string_append (stylesheet, base64); + g_free (base64); + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + g_object_set ( + G_OBJECT (settings), + "default-font-size", pango_font_description_get_size (vw) / PANGO_SCALE, + "default-font-family", pango_font_description_get_family (vw), + "monospace-font-family", pango_font_description_get_family (ms), + "default-monospace-font-size", (pango_font_description_get_size (ms) / PANGO_SCALE), + "user-stylesheet-uri", stylesheet->str, + NULL); + + g_string_free (stylesheet, TRUE); + + pango_font_description_free (ms); + pango_font_description_free (vw); +} + +/** + * e_html_editor_view_get_element_under_mouse_click: + * @view: an #EHTMLEditorView + * + * Returns DOM element, that was clicked on. + * + * Returns: DOM element on that was clicked. + */ +WebKitDOMElement * +e_html_editor_view_get_element_under_mouse_click (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), NULL); + + return view->priv->element_under_mouse; +} + +/** + * e_html_editor_view_check_magic_links + * @view: an #EHTMLEditorView + * @include_space: If TRUE the pattern for link expects space on end + * + * Check if actual selection in given editor is link. If so, it is surrounded + * with ANCHOR element. + */ +void +e_html_editor_view_check_magic_links (EHTMLEditorView *view, + gboolean include_space) +{ + WebKitDOMRange *range; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + range = html_editor_view_get_dom_range (view); + html_editor_view_check_magic_links (view, range, include_space, NULL); +} + +static CamelMimePart * +e_html_editor_view_add_inline_image_from_element (EHTMLEditorView *view, + WebKitDOMElement *element, + const gchar *attribute) +{ + CamelStream *stream; + CamelDataWrapper *wrapper; + CamelMimePart *part = NULL; + gsize decoded_size; + gssize size; + gchar *mime_type = NULL; + gchar *element_src, *cid, *name; + const gchar *base64_encoded_data; + guchar *base64_decoded_data; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) { + return NULL; + } + + element_src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (element), attribute); + + base64_encoded_data = strstr (element_src, ";base64,"); + if (!base64_encoded_data) + goto out; + + mime_type = g_strndup ( + element_src + 5, + base64_encoded_data - (strstr (element_src, "data:") + 5)); + + /* Move to actual data */ + base64_encoded_data += 8; + + base64_decoded_data = g_base64_decode (base64_encoded_data, &decoded_size); + + stream = camel_stream_mem_new (); + size = camel_stream_write ( + stream, (gchar *) base64_decoded_data, decoded_size, NULL, NULL); + + if (size == -1) + goto out; + + wrapper = camel_data_wrapper_new (); + camel_data_wrapper_construct_from_stream_sync ( + wrapper, stream, NULL, NULL); + g_object_unref (stream); + + camel_data_wrapper_set_mime_type (wrapper, mime_type); + + part = camel_mime_part_new (); + camel_medium_set_content (CAMEL_MEDIUM (part), wrapper); + g_object_unref (wrapper); + + cid = camel_header_msgid_generate (); + camel_mime_part_set_content_id (part, cid); + name = webkit_dom_element_get_attribute (element, "data-name"); + camel_mime_part_set_filename (part, name); + g_free (name); + camel_mime_part_set_encoding (part, CAMEL_TRANSFER_ENCODING_BASE64); +out: + g_free (mime_type); + g_free (element_src); + g_free (base64_decoded_data); + + return part; +} + +GList * +e_html_editor_view_get_parts_for_inline_images (EHTMLEditorView *view) +{ + GHashTable *added; + GList *parts = NULL; + gint length, ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + list = webkit_dom_document_query_selector_all (document, "img[data-inline]", NULL); + + length = webkit_dom_node_list_get_length (list); + if (length == 0) + return parts; + + added = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + for (ii = 0; ii < length; ii++) { + CamelMimePart *part; + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "src"); + + if (!g_hash_table_lookup (added, src)) { + part = e_html_editor_view_add_inline_image_from_element ( + view, WEBKIT_DOM_ELEMENT (node), "src"); + parts = g_list_append (parts, part); + g_hash_table_insert ( + added, src, (gpointer) camel_mime_part_get_content_id (part)); + } + g_free (src); + } + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + const gchar *id; + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "src"); + + if ((id = g_hash_table_lookup (added, src)) != NULL) { + gchar *cid = g_strdup_printf ("cid:%s", id); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), "src", cid, NULL); + g_free (cid); + } + g_free (src); + } + + list = webkit_dom_document_query_selector_all ( + document, "[data-inline][background]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + CamelMimePart *part; + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "background"); + + if (!g_hash_table_lookup (added, src)) { + part = e_html_editor_view_add_inline_image_from_element ( + view, WEBKIT_DOM_ELEMENT (node), "background"); + if (part) { + parts = g_list_append (parts, part); + g_hash_table_insert ( + added, src, + (gpointer) camel_mime_part_get_content_id (part)); + } + } + g_free (src); + } + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "background"); + const gchar *id; + + if ((id = g_hash_table_lookup (added, src)) != NULL) { + gchar *cid = g_strdup_printf ("cid:%s", id); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), "background", cid, NULL); + g_free (cid); + } + g_free (src); + } + + g_hash_table_destroy (added); + + return parts; +} + +/** + * e_html_editor_view_add_inline_image_from_mime_part: + * @composer: a composer object + * @part: a CamelMimePart containing image data + * + * This adds the mime part @part to @composer as an inline image. + **/ +void +e_html_editor_view_add_inline_image_from_mime_part (EHTMLEditorView *view, + CamelMimePart *part) +{ + CamelDataWrapper *dw; + CamelStream *stream; + GByteArray *byte_array; + gchar *src, *base64_encoded, *mime_type, *cid_src; + const gchar *cid, *name; + + stream = camel_stream_mem_new (); + dw = camel_medium_get_content (CAMEL_MEDIUM (part)); + g_return_if_fail (dw); + + mime_type = camel_data_wrapper_get_mime_type (dw); + camel_data_wrapper_decode_to_stream_sync (dw, stream, NULL, NULL); + camel_stream_close (stream, NULL, NULL); + + byte_array = camel_stream_mem_get_byte_array (CAMEL_STREAM_MEM (stream)); + + if (!byte_array->data) + return; + + base64_encoded = g_base64_encode ((const guchar *) byte_array->data, byte_array->len); + + name = camel_mime_part_get_filename (part); + /* Insert file name before new src */ + src = g_strconcat (name, ";data:", mime_type, ";base64,", base64_encoded, NULL); + + cid = camel_mime_part_get_content_id (part); + if (!cid) { + camel_mime_part_set_content_id (part, NULL); + cid = camel_mime_part_get_content_id (part); + } + cid_src = g_strdup_printf ("cid:%s", cid); + + g_hash_table_insert (view->priv->inline_images, cid_src, src); + + g_free (base64_encoded); + g_free (mime_type); + g_object_unref (stream); +} diff --git a/e-util/e-html-editor-view.h b/e-util/e-html-editor-view.h new file mode 100644 index 0000000000..e40b15b375 --- /dev/null +++ b/e-util/e-html-editor-view.h @@ -0,0 +1,164 @@ +/* + * e-html-editor-view.h + * + * 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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_VIEW_H +#define E_HTML_EDITOR_VIEW_H + +#include <webkit/webkit.h> + +#include <camel/camel.h> + +#include <e-util/e-html-editor-selection.h> +#include <e-util/e-emoticon.h> +#include <e-util/e-spell-checker.h> +#include <e-util/e-util-enums.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR_VIEW \ + (e_html_editor_view_get_type ()) +#define E_HTML_EDITOR_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR_VIEW, EHTMLEditorView)) +#define E_HTML_EDITOR_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR_VIEW, EHTMLEditorViewClass)) +#define E_IS_HTML_EDITOR_VIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR_VIEW)) +#define E_IS_HTML_EDITOR_VIEW_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR_VIEW)) +#define E_HTML_EDITOR_VIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR_VIEW, EHTMLEditorViewClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditorView EHTMLEditorView; +typedef struct _EHTMLEditorViewClass EHTMLEditorViewClass; +typedef struct _EHTMLEditorViewPrivate EHTMLEditorViewPrivate; + +struct _EHTMLEditorView { + WebKitWebView parent; + EHTMLEditorViewPrivate *priv; +}; + +struct _EHTMLEditorViewClass { + WebKitWebViewClass parent_class; + + void (*paste_clipboard_quoted) + (EHTMLEditorView *view); + gboolean (*popup_event) (EHTMLEditorView *view, + GdkEventButton *event); + void (*paste_primary_clipboard) + (EHTMLEditorView *view); +}; + +GType e_html_editor_view_get_type (void) G_GNUC_CONST; +EHTMLEditorView * + e_html_editor_view_new (void); +EHTMLEditorSelection * + e_html_editor_view_get_selection + (EHTMLEditorView *view); +gboolean e_html_editor_view_exec_command (EHTMLEditorView *view, + EHTMLEditorViewCommand command, + const gchar *value); +gboolean e_html_editor_view_get_changed (EHTMLEditorView *view); +void e_html_editor_view_set_changed (EHTMLEditorView *view, + gboolean changed); +gboolean e_html_editor_view_get_html_mode + (EHTMLEditorView *view); +void e_html_editor_view_set_html_mode + (EHTMLEditorView *view, + gboolean html_mode); +gboolean e_html_editor_view_get_inline_spelling + (EHTMLEditorView *view); +void e_html_editor_view_set_inline_spelling + (EHTMLEditorView *view, + gboolean inline_spelling); +gboolean e_html_editor_view_get_magic_links + (EHTMLEditorView *view); +void e_html_editor_view_set_magic_links + (EHTMLEditorView *view, + gboolean magic_links); +void e_html_editor_view_insert_smiley + (EHTMLEditorView *view, + EEmoticon *emoticon); +gboolean e_html_editor_view_get_magic_smileys + (EHTMLEditorView *view); +void e_html_editor_view_set_magic_smileys + (EHTMLEditorView *view, + gboolean magic_smileys); +ESpellChecker * e_html_editor_view_get_spell_checker + (EHTMLEditorView *view); +gchar * e_html_editor_view_get_text_html + (EHTMLEditorView *view); +gchar * e_html_editor_view_get_text_html_for_drafts + (EHTMLEditorView *view); +gchar * e_html_editor_view_get_text_plain + (EHTMLEditorView *view); +void e_html_editor_view_convert_and_insert_html_to_plain_text + (EHTMLEditorView *view, + const gchar *html); +void e_html_editor_view_set_text_html + (EHTMLEditorView *view, + const gchar *text); +void e_html_editor_view_set_text_plain + (EHTMLEditorView *view, + const gchar *text); +void e_html_editor_view_paste_clipboard_quoted + (EHTMLEditorView *view); +void e_html_editor_view_embed_styles (EHTMLEditorView *view); +void e_html_editor_view_remove_embed_styles + (EHTMLEditorView *view); +void e_html_editor_view_update_fonts (EHTMLEditorView *view); +WebKitDOMElement * + e_html_editor_view_get_element_under_mouse_click + (EHTMLEditorView *view); +void e_html_editor_view_check_magic_links + (EHTMLEditorView *view, + gboolean while_typing); +WebKitDOMElement * + e_html_editor_view_quote_plain_text_element + (EHTMLEditorView *view, + WebKitDOMElement *element); +WebKitDOMElement * + e_html_editor_view_quote_plain_text + (EHTMLEditorView *view); +void e_html_editor_view_dequote_plain_text + (EHTMLEditorView *view); +void e_html_editor_view_turn_spell_check_off + (EHTMLEditorView *view); +void e_html_editor_view_force_spell_check_for_current_paragraph + (EHTMLEditorView *view); +void e_html_editor_view_force_spell_check + (EHTMLEditorView *view); +void e_html_editor_view_add_inline_image_from_mime_part + (EHTMLEditorView *view, + CamelMimePart *part); +GList * e_html_editor_view_get_parts_for_inline_images + (EHTMLEditorView *view); +G_END_DECLS + +#endif /* E_HTML_EDITOR_VIEW_H */ 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; +} + diff --git a/e-util/e-html-editor.h b/e-util/e-html-editor.h new file mode 100644 index 0000000000..5618cc8e05 --- /dev/null +++ b/e-util/e-html-editor.h @@ -0,0 +1,108 @@ +/* + * e-html-editor.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_HTML_EDITOR_H +#define E_HTML_EDITOR_H + +#include <gtk/gtk.h> +#include <e-util/e-activity.h> +#include <e-util/e-activity-bar.h> +#include <e-util/e-html-editor-view.h> + +/* Standard GObject macros */ +#define E_TYPE_HTML_EDITOR \ + (e_html_editor_get_type ()) +#define E_HTML_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_HTML_EDITOR, EHTMLEditor)) +#define E_HTML_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_HTML_EDITOR, EHTMLEditorClass)) +#define E_IS_HTML_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_HTML_EDITOR)) +#define E_IS_HTML_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_HTML_EDITOR)) +#define E_HTML_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_HTML_EDITOR, EHTMLEditorClass)) + +G_BEGIN_DECLS + +typedef struct _EHTMLEditor EHTMLEditor; +typedef struct _EHTMLEditorClass EHTMLEditorClass; +typedef struct _EHTMLEditorPrivate EHTMLEditorPrivate; + +struct _EHTMLEditor { + GtkGrid parent; + EHTMLEditorPrivate *priv; +}; + +struct _EHTMLEditorClass { + GtkGridClass parent_class; + + void (*update_actions) (EHTMLEditor *editor, + GdkEventButton *event); + void (*spell_languages_changed) + (EHTMLEditor *editor); +}; + +GType e_html_editor_get_type (void) G_GNUC_CONST; +GtkWidget * e_html_editor_new (void); +EHTMLEditorView * + e_html_editor_get_view (EHTMLEditor *editor); +GtkBuilder * e_html_editor_get_builder (EHTMLEditor *editor); +GtkUIManager * e_html_editor_get_ui_manager (EHTMLEditor *editor); +GtkAction * e_html_editor_get_action (EHTMLEditor *editor, + const gchar *action_name); +GtkActionGroup *e_html_editor_get_action_group (EHTMLEditor *editor, + const gchar *group_name); +GtkWidget * e_html_editor_get_widget (EHTMLEditor *editor, + const gchar *widget_name); +GtkWidget * e_html_editor_get_managed_widget + (EHTMLEditor *editor, + const gchar *widget_path); +GtkWidget * e_html_editor_get_style_combo_box + (EHTMLEditor *editor); +const gchar * e_html_editor_get_filename (EHTMLEditor *editor); +void e_html_editor_set_filename (EHTMLEditor *editor, + const gchar *filename); +EActivityBar * e_html_editor_get_activity_bar (EHTMLEditor *editor); +EActivity * e_html_editor_new_activity (EHTMLEditor *editor); +void e_html_editor_pack_above (EHTMLEditor *editor, + GtkWidget *child); + +/***************************************************************************** + * High-Level Editing Interface + *****************************************************************************/ + +gboolean e_html_editor_save (EHTMLEditor *editor, + const gchar *filename, + gboolean as_html, + GError **error); + +G_END_DECLS + +#endif /* E_HTML_EDITOR_H */ diff --git a/e-util/e-image-chooser-dialog.c b/e-util/e-image-chooser-dialog.c new file mode 100644 index 0000000000..73a6c202cb --- /dev/null +++ b/e-util/e-image-chooser-dialog.c @@ -0,0 +1,223 @@ +/* + * e-image-chooser-dialog.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 "e-image-chooser-dialog.h" + +#define E_IMAGE_CHOOSER_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_IMAGE_CHOOSER_DIALOG, EImageChooserDialogPrivate)) + +#define PREVIEW_WIDTH 256 +#define PREVIEW_HEIGHT 256 + +typedef struct _Context Context; + +struct _EImageChooserDialogPrivate { + GCancellable *cancellable; +}; + +struct _Context { + GtkFileChooser *file_chooser; + GCancellable *cancellable; +}; + +G_DEFINE_TYPE ( + EImageChooserDialog, + e_image_chooser_dialog, + GTK_TYPE_FILE_CHOOSER_DIALOG) + +static void +context_free (Context *context) +{ + g_object_unref (context->file_chooser); + g_object_unref (context->cancellable); + + g_slice_free (Context, context); +} + +static void +image_chooser_dialog_read_cb (GFile *preview_file, + GAsyncResult *result, + Context *context) +{ + GdkPixbuf *pixbuf; + GtkWidget *preview_widget; + GFileInputStream *input_stream; + + input_stream = g_file_read_finish (preview_file, result, NULL); + + /* FIXME Handle errors better, but remember + * to ignore G_IO_ERROR_CANCELLED. */ + if (input_stream == NULL) + goto exit; + + /* XXX This blocks, but GDK-PixBuf offers no asynchronous + * alternative and I don't want to deal with making GDK + * calls from threads and all the crap that goes with it. */ + pixbuf = gdk_pixbuf_new_from_stream_at_scale ( + G_INPUT_STREAM (input_stream), + PREVIEW_WIDTH, PREVIEW_HEIGHT, TRUE, + context->cancellable, NULL); + + preview_widget = gtk_file_chooser_get_preview_widget ( + context->file_chooser); + + gtk_file_chooser_set_preview_widget_active ( + context->file_chooser, pixbuf != NULL); + + gtk_image_set_from_pixbuf (GTK_IMAGE (preview_widget), pixbuf); + + if (pixbuf != NULL) + g_object_unref (pixbuf); + + g_object_unref (input_stream); + +exit: + context_free (context); +} + +static void +image_chooser_dialog_update_preview (GtkFileChooser *file_chooser) +{ + EImageChooserDialogPrivate *priv; + GtkWidget *preview_widget; + GFile *preview_file; + Context *context; + + priv = E_IMAGE_CHOOSER_DIALOG_GET_PRIVATE (file_chooser); + preview_file = gtk_file_chooser_get_preview_file (file_chooser); + preview_widget = gtk_file_chooser_get_preview_widget (file_chooser); + + if (priv->cancellable != NULL) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + gtk_image_clear (GTK_IMAGE (preview_widget)); + gtk_file_chooser_set_preview_widget_active (file_chooser, FALSE); + + if (preview_file == NULL) + return; + + priv->cancellable = g_cancellable_new (); + + context = g_slice_new0 (Context); + context->file_chooser = g_object_ref (file_chooser); + context->cancellable = g_object_ref (priv->cancellable); + + g_file_read_async ( + preview_file, G_PRIORITY_LOW, + priv->cancellable, (GAsyncReadyCallback) + image_chooser_dialog_read_cb, context); + + g_object_unref (preview_file); +} + +static void +image_chooser_dialog_dispose (GObject *object) +{ + EImageChooserDialogPrivate *priv; + + priv = E_IMAGE_CHOOSER_DIALOG_GET_PRIVATE (object); + + if (priv->cancellable != NULL) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_image_chooser_dialog_parent_class)->dispose (object); +} + +static void +image_chooser_dialog_constructed (GObject *object) +{ + GtkFileChooser *file_chooser; + GtkFileFilter *file_filter; + + file_chooser = GTK_FILE_CHOOSER (object); + gtk_file_chooser_set_local_only (file_chooser, FALSE); + + gtk_dialog_add_button ( + GTK_DIALOG (file_chooser), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + gtk_dialog_add_button ( + GTK_DIALOG (file_chooser), + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT); + gtk_dialog_set_default_response ( + GTK_DIALOG (file_chooser), GTK_RESPONSE_ACCEPT); + + file_filter = gtk_file_filter_new (); + gtk_file_filter_add_pixbuf_formats (file_filter); + gtk_file_chooser_set_filter (file_chooser, file_filter); + + gtk_file_chooser_set_preview_widget (file_chooser, gtk_image_new ()); +} + +static void +e_image_chooser_dialog_class_init (EImageChooserDialogClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EImageChooserDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = image_chooser_dialog_dispose; + object_class->constructed = image_chooser_dialog_constructed; +} + +static void +e_image_chooser_dialog_init (EImageChooserDialog *dialog) +{ + dialog->priv = E_IMAGE_CHOOSER_DIALOG_GET_PRIVATE (dialog); + + g_signal_connect ( + dialog, "update-preview", + G_CALLBACK (image_chooser_dialog_update_preview), NULL); +} + +GtkWidget * +e_image_chooser_dialog_new (const gchar *title, + GtkWindow *parent) +{ + return g_object_new ( + E_TYPE_IMAGE_CHOOSER_DIALOG, + "action", GTK_FILE_CHOOSER_ACTION_OPEN, + "title", title, + "transient-for", parent, NULL); +} + +GFile * +e_image_chooser_dialog_run (EImageChooserDialog *dialog) +{ + GtkFileChooser *file_chooser; + + g_return_val_if_fail (E_IS_IMAGE_CHOOSER_DIALOG (dialog), NULL); + + file_chooser = GTK_FILE_CHOOSER (dialog); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) != GTK_RESPONSE_ACCEPT) + return NULL; + + return gtk_file_chooser_get_file (file_chooser); +} diff --git a/e-util/e-image-chooser-dialog.h b/e-util/e-image-chooser-dialog.h new file mode 100644 index 0000000000..967fddfbad --- /dev/null +++ b/e-util/e-image-chooser-dialog.h @@ -0,0 +1,74 @@ +/* + * e-image-chooser-dialog.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_IMAGE_CHOOSER_DIALOG_H +#define E_IMAGE_CHOOSER_DIALOG_H + +#include <gtk/gtk.h> + +/* Standard GObject macros */ +#define E_TYPE_IMAGE_CHOOSER_DIALOG \ + (e_image_chooser_dialog_get_type ()) +#define E_IMAGE_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_IMAGE_CHOOSER_DIALOG, EImageChooserDialog)) +#define E_IMAGE_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_IMAGE_CHOOSER_DIALOG, EImageChooserDialogClass)) +#define E_IS_IMAGE_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_IMAGE_CHOOSER_DIALOG)) +#define E_IS_IMAGE_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_IMAGE_CHOOSER_DIALOG)) +#define E_IMAGE_CHOOSER_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_IMAGE_CHOOSER_DIALOG, EImageChooserDialogClass)) + +G_BEGIN_DECLS + +typedef struct _EImageChooserDialog EImageChooserDialog; +typedef struct _EImageChooserDialogClass EImageChooserDialogClass; +typedef struct _EImageChooserDialogPrivate EImageChooserDialogPrivate; + +struct _EImageChooserDialog { + GtkFileChooserDialog parent; + EImageChooserDialogPrivate *priv; +}; + +struct _EImageChooserDialogClass { + GtkFileChooserDialogClass parent_class; +}; + +GType e_image_chooser_dialog_get_type + (void) G_GNUC_CONST; +GtkWidget * e_image_chooser_dialog_new + (const gchar *title, + GtkWindow *parent); +GFile * e_image_chooser_dialog_run + (EImageChooserDialog *dialog); + +G_END_DECLS + +#endif /* E_IMAGE_CHOOSER_DIALOG_H */ diff --git a/e-util/e-mail-signature-editor.c b/e-util/e-mail-signature-editor.c index 05c783d448..b5c87635f1 100644 --- a/e-util/e-mail-signature-editor.c +++ b/e-util/e-mail-signature-editor.c @@ -17,13 +17,13 @@ #include "e-mail-signature-editor.h" +#include <config.h> #include <string.h> #include <glib/gi18n.h> -#include "e-alert-bar.h" #include "e-alert-dialog.h" #include "e-alert-sink.h" -#include "e-web-view-gtkhtml.h" +#include "e-alert-bar.h" #define E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ @@ -32,6 +32,7 @@ typedef struct _AsyncContext AsyncContext; struct _EMailSignatureEditorPrivate { + EHTMLEditor *editor; GtkActionGroup *action_group; EFocusTracker *focus_tracker; GCancellable *cancellable; @@ -40,7 +41,6 @@ struct _EMailSignatureEditorPrivate { gchar *original_name; GtkWidget *entry; /* not referenced */ - GtkWidget *alert_bar; /* not referenced */ }; struct _AsyncContext { @@ -52,6 +52,7 @@ struct _AsyncContext { enum { PROP_0, + PROP_EDITOR, PROP_FOCUS_TRACKER, PROP_REGISTRY, PROP_SOURCE @@ -75,17 +76,10 @@ static const gchar *ui = " </toolbar>\n" "</ui>"; -/* Forward Declarations */ -static void e_mail_signature_editor_alert_sink_init - (EAlertSinkInterface *iface); - -G_DEFINE_TYPE_WITH_CODE ( +G_DEFINE_TYPE ( EMailSignatureEditor, e_mail_signature_editor, - GTKHTML_TYPE_EDITOR, - G_IMPLEMENT_INTERFACE ( - E_TYPE_ALERT_SINK, - e_mail_signature_editor_alert_sink_init)) + GTK_TYPE_WINDOW) static void async_context_free (AsyncContext *async_context) @@ -106,8 +100,10 @@ mail_signature_editor_loaded_cb (GObject *object, GAsyncResult *result, gpointer user_data) { + EHTMLEditor *editor; + EHTMLEditorView *view; ESource *source; - EMailSignatureEditor *editor; + EMailSignatureEditor *window; ESourceMailSignature *extension; const gchar *extension_name; const gchar *mime_type; @@ -116,7 +112,7 @@ mail_signature_editor_loaded_cb (GObject *object, GError *error = NULL; source = E_SOURCE (object); - editor = E_MAIL_SIGNATURE_EDITOR (user_data); + window = E_MAIL_SIGNATURE_EDITOR (user_data); e_source_mail_signature_load_finish ( source, result, &contents, NULL, &error); @@ -124,17 +120,17 @@ mail_signature_editor_loaded_cb (GObject *object, /* Ignore cancellations. */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_warn_if_fail (contents == NULL); - g_object_unref (editor); + g_object_unref (window); g_error_free (error); return; } else if (error != NULL) { g_warn_if_fail (contents == NULL); e_alert_submit ( - E_ALERT_SINK (editor), + E_ALERT_SINK (window), "widgets:no-load-signature", error->message, NULL); - g_object_unref (editor); + g_object_unref (window); g_error_free (error); return; } @@ -147,30 +143,18 @@ mail_signature_editor_loaded_cb (GObject *object, mime_type = e_source_mail_signature_get_mime_type (extension); is_html = (g_strcmp0 (mime_type, "text/html") == 0); - gtkhtml_editor_set_html_mode (GTKHTML_EDITOR (editor), is_html); + editor = e_mail_signature_editor_get_editor (window); + view = e_html_editor_get_view (editor); + e_html_editor_view_set_html_mode (view, is_html); - if (is_html) { - gtkhtml_editor_insert_html ( - GTKHTML_EDITOR (editor), contents); - } else { - gtkhtml_editor_insert_text ( - GTKHTML_EDITOR (editor), contents); - - gtkhtml_editor_run_command ( - GTKHTML_EDITOR (editor), "cursor-position-save"); - gtkhtml_editor_run_command ( - GTKHTML_EDITOR (editor), "select-all"); - gtkhtml_editor_run_command ( - GTKHTML_EDITOR (editor), "style-pre"); - gtkhtml_editor_run_command ( - GTKHTML_EDITOR (editor), "unselect-all"); - gtkhtml_editor_run_command ( - GTKHTML_EDITOR (editor), "cursor-position-restore"); - } + if (is_html) + e_html_editor_view_set_text_html (view, contents); + else + e_html_editor_view_set_text_plain (view, contents); g_free (contents); - g_object_unref (editor); + g_object_unref (window); } static gboolean @@ -189,28 +173,33 @@ mail_signature_editor_delete_event_cb (EMailSignatureEditor *editor, static void action_close_cb (GtkAction *action, - EMailSignatureEditor *editor) + EMailSignatureEditor *window) { + EHTMLEditor *editor; + EHTMLEditorView *view; gboolean something_changed = FALSE; const gchar *original_name; const gchar *signature_name; - original_name = editor->priv->original_name; - signature_name = gtk_entry_get_text (GTK_ENTRY (editor->priv->entry)); + original_name = window->priv->original_name; + signature_name = gtk_entry_get_text (GTK_ENTRY (window->priv->entry)); + + editor = e_mail_signature_editor_get_editor (window); + view = e_html_editor_get_view (editor); - something_changed |= gtkhtml_editor_has_undo (GTKHTML_EDITOR (editor)); + something_changed |= webkit_web_view_can_undo (WEBKIT_WEB_VIEW (view)); something_changed |= (strcmp (signature_name, original_name) != 0); if (something_changed) { gint response; response = e_alert_run_dialog_for_args ( - GTK_WINDOW (editor), + GTK_WINDOW (window), "widgets:ask-signature-changed", NULL); if (response == GTK_RESPONSE_YES) { GtkActionGroup *action_group; - action_group = editor->priv->action_group; + action_group = window->priv->action_group; action = gtk_action_group_get_action ( action_group, "save-and-close"); gtk_action_activate (action); @@ -219,7 +208,7 @@ action_close_cb (GtkAction *action, return; } - gtk_widget_destroy (GTK_WIDGET (editor)); + gtk_widget_destroy (GTK_WIDGET (window)); } static void @@ -292,8 +281,7 @@ action_save_and_close_cb (GtkAction *action, /* Only make sure that the 'source-changed' is called, * thus the preview of the signature is updated on save. * It is not called when only signature body is changed - * (and ESource properties are left unchanged). - */ + * (and ESource properties are left unchanged). */ g_signal_emit_by_name (registry, "source-changed", source); gtk_widget_destroy (GTK_WIDGET (editor)); @@ -397,6 +385,13 @@ mail_signature_editor_get_property (GObject *object, GParamSpec *pspec) { switch (property_id) { + case PROP_EDITOR: + g_value_set_object ( + value, + e_mail_signature_editor_get_editor ( + E_MAIL_SIGNATURE_EDITOR (object))); + return; + case PROP_FOCUS_TRACKER: g_value_set_object ( value, @@ -429,6 +424,11 @@ mail_signature_editor_dispose (GObject *object) priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (object); + if (priv->editor != NULL) { + g_object_unref (priv->editor); + priv->editor = NULL; + } + if (priv->action_group != NULL) { g_object_unref (priv->action_group); priv->action_group = NULL; @@ -477,16 +477,18 @@ mail_signature_editor_finalize (GObject *object) static void mail_signature_editor_constructed (GObject *object) { - EMailSignatureEditor *editor; + EMailSignatureEditor *window; GtkActionGroup *action_group; EFocusTracker *focus_tracker; - GtkhtmlEditor *gtkhtml_editor; + EHTMLEditor *editor; + EHTMLEditorView *view; GtkUIManager *ui_manager; GDBusObject *dbus_object; ESource *source; GtkAction *action; GtkWidget *container; GtkWidget *widget; + GtkWidget *hbox; const gchar *display_name; GError *error = NULL; @@ -494,10 +496,11 @@ mail_signature_editor_constructed (GObject *object) G_OBJECT_CLASS (e_mail_signature_editor_parent_class)-> constructed (object); - editor = E_MAIL_SIGNATURE_EDITOR (object); + window = E_MAIL_SIGNATURE_EDITOR (object); + editor = e_mail_signature_editor_get_editor (window); + view = e_html_editor_get_view (editor); - gtkhtml_editor = GTKHTML_EDITOR (editor); - ui_manager = gtkhtml_editor_get_ui_manager (gtkhtml_editor); + ui_manager = e_html_editor_get_ui_manager (editor); /* Because we are loading from a hard-coded string, there is * no chance of I/O errors. Failure here implies a malformed @@ -511,103 +514,102 @@ mail_signature_editor_constructed (GObject *object) action_group, GETTEXT_PACKAGE); gtk_action_group_add_actions ( action_group, entries, - G_N_ELEMENTS (entries), editor); + G_N_ELEMENTS (entries), window); gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - editor->priv->action_group = g_object_ref (action_group); + window->priv->action_group = g_object_ref (action_group); /* Hide page properties because it is not inherited in the mail. */ - action = gtkhtml_editor_get_action (gtkhtml_editor, "properties-page"); + action = e_html_editor_get_action (editor, "properties-page"); gtk_action_set_visible (action, FALSE); - action = gtkhtml_editor_get_action ( - gtkhtml_editor, "context-properties-page"); + action = e_html_editor_get_action (editor, "context-properties-page"); gtk_action_set_visible (action, FALSE); gtk_ui_manager_ensure_update (ui_manager); - gtk_window_set_title (GTK_WINDOW (editor), _("Edit Signature")); + gtk_window_set_title (GTK_WINDOW (window), _("Edit Signature")); + gtk_window_set_default_size (GTK_WINDOW (window), 600, 440); - /* Construct the signature name entry. */ + widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add (GTK_CONTAINER (window), widget); + gtk_widget_show (widget); + + container = widget; + + /* Construct the main menu and toolbar. */ + + widget = e_html_editor_get_managed_widget (editor, "/main-menu"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = e_html_editor_get_managed_widget (editor, "/main-toolbar"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); - container = gtkhtml_editor->vbox; + /* Construct the signature name entry. */ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_container_set_border_width (GTK_CONTAINER (widget), 6); gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); - /* Position 2 should be between the main and style toolbars. */ - gtk_box_reorder_child (GTK_BOX (container), widget, 2); gtk_widget_show (widget); - container = widget; + hbox = widget; widget = gtk_entry_new (); - gtk_box_pack_end (GTK_BOX (container), widget, TRUE, TRUE, 0); - editor->priv->entry = widget; /* not referenced */ + gtk_box_pack_end (GTK_BOX (hbox), widget, TRUE, TRUE, 0); + window->priv->entry = widget; /* not referenced */ gtk_widget_show (widget); widget = gtk_label_new_with_mnemonic (_("_Signature Name:")); - gtk_label_set_mnemonic_widget (GTK_LABEL (widget), editor->priv->entry); - gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_label_set_mnemonic_widget (GTK_LABEL (widget), window->priv->entry); + gtk_box_pack_end (GTK_BOX (hbox), widget, FALSE, FALSE, 0); gtk_widget_show (widget); - g_signal_connect ( - editor, "delete-event", - G_CALLBACK (mail_signature_editor_delete_event_cb), NULL); - - /* Construct the alert bar for errors. */ + /* Construct the main editing area. */ - container = gtkhtml_editor->vbox; - - widget = e_alert_bar_new (); - gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); - /* Position 5 should be between the style toolbar and editing area. */ - gtk_box_reorder_child (GTK_BOX (container), widget, 5); - editor->priv->alert_bar = widget; /* not referenced */ - /* EAlertBar controls its own visibility. */ + widget = GTK_WIDGET (editor); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); - /* Configure an EFocusTracker to manage selection actions. - * - * XXX GtkhtmlEditor does not manage its own selection actions, - * which is technically a bug but works in our favor here - * because it won't cause any conflicts with EFocusTracker. */ + g_signal_connect ( + window, "delete-event", + G_CALLBACK (mail_signature_editor_delete_event_cb), NULL); - focus_tracker = e_focus_tracker_new (GTK_WINDOW (editor)); + /* Configure an EFocusTracker to manage selection actions. */ + focus_tracker = e_focus_tracker_new (GTK_WINDOW (window)); - action = gtkhtml_editor_get_action (gtkhtml_editor, "cut"); + action = e_html_editor_get_action (editor, "cut"); e_focus_tracker_set_cut_clipboard_action (focus_tracker, action); - action = gtkhtml_editor_get_action (gtkhtml_editor, "copy"); + action = e_html_editor_get_action (editor, "copy"); e_focus_tracker_set_copy_clipboard_action (focus_tracker, action); - action = gtkhtml_editor_get_action (gtkhtml_editor, "paste"); + action = e_html_editor_get_action (editor, "paste"); e_focus_tracker_set_paste_clipboard_action (focus_tracker, action); - action = gtkhtml_editor_get_action (gtkhtml_editor, "select-all"); + action = e_html_editor_get_action (editor, "select-all"); e_focus_tracker_set_select_all_action (focus_tracker, action); - editor->priv->focus_tracker = focus_tracker; + window->priv->focus_tracker = focus_tracker; - source = e_mail_signature_editor_get_source (editor); + source = e_mail_signature_editor_get_source (window); display_name = e_source_get_display_name (source); if (display_name == NULL || *display_name == '\0') display_name = _("Unnamed"); /* Set the entry text before we grab focus. */ - g_free (editor->priv->original_name); - editor->priv->original_name = g_strdup (display_name); - gtk_entry_set_text (GTK_ENTRY (editor->priv->entry), display_name); + g_free (window->priv->original_name); + window->priv->original_name = g_strdup (display_name); + gtk_entry_set_text (GTK_ENTRY (window->priv->entry), display_name); /* Set the focus appropriately. If this is a new signature, draw * the user's attention to the signature name entry. Otherwise go * straight to the editing area. */ if (source == NULL) { - gtk_widget_grab_focus (editor->priv->entry); + gtk_widget_grab_focus (window->priv->entry); } else { - GtkHTML *html; - - html = gtkhtml_editor_get_html (gtkhtml_editor); - gtk_widget_grab_focus (GTK_WIDGET (html)); + gtk_widget_grab_focus (GTK_WIDGET (view)); } /* Load file content only for an existing signature. @@ -623,72 +625,19 @@ mail_signature_editor_constructed (GObject *object) G_PRIORITY_DEFAULT, cancellable, mail_signature_editor_loaded_cb, - g_object_ref (editor)); + g_object_ref (window)); - g_warn_if_fail (editor->priv->cancellable == NULL); - editor->priv->cancellable = cancellable; + g_warn_if_fail (window->priv->cancellable == NULL); + window->priv->cancellable = cancellable; g_object_unref (dbus_object); } } static void -mail_signature_editor_cut_clipboard (GtkhtmlEditor *editor) -{ - /* Do nothing. EFocusTracker handles this. */ -} - -static void -mail_signature_editor_copy_clipboard (GtkhtmlEditor *editor) -{ - /* Do nothing. EFocusTracker handles this. */ -} - -static void -mail_signature_editor_paste_clipboard (GtkhtmlEditor *editor) -{ - /* Do nothing. EFocusTracker handles this. */ -} - -static void -mail_signature_editor_select_all (GtkhtmlEditor *editor) -{ - /* Do nothing. EFocusTracker handles this. */ -} - -static void -mail_signature_editor_submit_alert (EAlertSink *alert_sink, - EAlert *alert) -{ - EMailSignatureEditorPrivate *priv; - EAlertBar *alert_bar; - GtkWidget *dialog; - GtkWindow *parent; - - priv = E_MAIL_SIGNATURE_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: - parent = GTK_WINDOW (alert_sink); - dialog = e_alert_dialog_new (parent, alert); - gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy (dialog); - break; - } -} - -static void e_mail_signature_editor_class_init (EMailSignatureEditorClass *class) { GObjectClass *object_class; - GtkhtmlEditorClass *editor_class; g_type_class_add_private (class, sizeof (EMailSignatureEditorPrivate)); @@ -699,11 +648,16 @@ e_mail_signature_editor_class_init (EMailSignatureEditorClass *class) object_class->finalize = mail_signature_editor_finalize; object_class->constructed = mail_signature_editor_constructed; - editor_class = GTKHTML_EDITOR_CLASS (class); - editor_class->cut_clipboard = mail_signature_editor_cut_clipboard; - editor_class->copy_clipboard = mail_signature_editor_copy_clipboard; - editor_class->paste_clipboard = mail_signature_editor_paste_clipboard; - editor_class->select_all = mail_signature_editor_select_all; + g_object_class_install_property ( + object_class, + PROP_EDITOR, + g_param_spec_object ( + "editor", + NULL, + NULL, + E_TYPE_HTML_EDITOR, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); g_object_class_install_property ( object_class, @@ -742,15 +696,11 @@ e_mail_signature_editor_class_init (EMailSignatureEditorClass *class) } static void -e_mail_signature_editor_alert_sink_init (EAlertSinkInterface *iface) -{ - iface->submit_alert = mail_signature_editor_submit_alert; -} - -static void e_mail_signature_editor_init (EMailSignatureEditor *editor) { editor->priv = E_MAIL_SIGNATURE_EDITOR_GET_PRIVATE (editor); + + editor->priv->editor = g_object_ref_sink (e_html_editor_new ()); } GtkWidget * @@ -764,11 +714,18 @@ e_mail_signature_editor_new (ESourceRegistry *registry, return g_object_new ( E_TYPE_MAIL_SIGNATURE_EDITOR, - "html", e_web_view_gtkhtml_new (), "registry", registry, "source", source, NULL); } +EHTMLEditor * +e_mail_signature_editor_get_editor (EMailSignatureEditor *editor) +{ + g_return_val_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor), NULL); + + return editor->priv->editor; +} + EFocusTracker * e_mail_signature_editor_get_focus_tracker (EMailSignatureEditor *editor) { @@ -851,7 +808,7 @@ mail_signature_editor_commit_cb (GObject *object, } void -e_mail_signature_editor_commit (EMailSignatureEditor *editor, +e_mail_signature_editor_commit (EMailSignatureEditor *window, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) @@ -864,23 +821,23 @@ e_mail_signature_editor_commit (EMailSignatureEditor *editor, const gchar *extension_name; const gchar *mime_type; gchar *contents; - gboolean is_html; - gsize length; + EHTMLEditor *editor; + EHTMLEditorView *view; - g_return_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (editor)); + g_return_if_fail (E_IS_MAIL_SIGNATURE_EDITOR (window)); - registry = e_mail_signature_editor_get_registry (editor); - source = e_mail_signature_editor_get_source (editor); - is_html = gtkhtml_editor_get_html_mode (GTKHTML_EDITOR (editor)); + registry = e_mail_signature_editor_get_registry (window); + source = e_mail_signature_editor_get_source (window); + + editor = e_mail_signature_editor_get_editor (window); + view = e_html_editor_get_view (editor); - if (is_html) { + if (e_html_editor_view_get_html_mode (view)) { mime_type = "text/html"; - contents = gtkhtml_editor_get_text_html ( - GTKHTML_EDITOR (editor), &length); + contents = e_html_editor_view_get_text_html (view); } else { mime_type = "text/plain"; - contents = gtkhtml_editor_get_text_plain ( - GTKHTML_EDITOR (editor), &length); + contents = e_html_editor_view_get_text_plain (view); } extension_name = E_SOURCE_EXTENSION_MAIL_SIGNATURE; @@ -890,13 +847,13 @@ e_mail_signature_editor_commit (EMailSignatureEditor *editor, async_context = g_slice_new0 (AsyncContext); async_context->source = g_object_ref (source); async_context->contents = contents; /* takes ownership */ - async_context->length = length; + async_context->length = strlen (contents); if (G_IS_CANCELLABLE (cancellable)) async_context->cancellable = g_object_ref (cancellable); simple = g_simple_async_result_new ( - G_OBJECT (editor), callback, user_data, + G_OBJECT (window), callback, user_data, e_mail_signature_editor_commit); g_simple_async_result_set_op_res_gpointer ( diff --git a/e-util/e-mail-signature-editor.h b/e-util/e-mail-signature-editor.h index 7de6841647..1b8622d5d1 100644 --- a/e-util/e-mail-signature-editor.h +++ b/e-util/e-mail-signature-editor.h @@ -22,9 +22,9 @@ #ifndef E_MAIL_SIGNATURE_EDITOR_H #define E_MAIL_SIGNATURE_EDITOR_H -#include <gtkhtml-editor.h> #include <libedataserver/libedataserver.h> +#include <e-util/e-html-editor.h> #include <e-util/e-focus-tracker.h> /* Standard GObject macros */ @@ -53,18 +53,20 @@ typedef struct _EMailSignatureEditorClass EMailSignatureEditorClass; typedef struct _EMailSignatureEditorPrivate EMailSignatureEditorPrivate; struct _EMailSignatureEditor { - GtkhtmlEditor parent; + GtkWindow parent; EMailSignatureEditorPrivate *priv; }; struct _EMailSignatureEditorClass { - GtkhtmlEditorClass parent_class; + GtkWindowClass parent_class; }; GType e_mail_signature_editor_get_type (void) G_GNUC_CONST; GtkWidget * e_mail_signature_editor_new (ESourceRegistry *registry, ESource *source); +EHTMLEditor * e_mail_signature_editor_get_editor + (EMailSignatureEditor *editor); EFocusTracker * e_mail_signature_editor_get_focus_tracker (EMailSignatureEditor *editor); ESourceRegistry * diff --git a/e-util/e-mail-signature-manager.c b/e-util/e-mail-signature-manager.c index 7f75cb7540..fb34c7eb4c 100644 --- a/e-util/e-mail-signature-manager.c +++ b/e-util/e-mail-signature-manager.c @@ -398,15 +398,22 @@ mail_signature_manager_constructed (GObject *object) static void mail_signature_manager_add_signature (EMailSignatureManager *manager) { + EHTMLEditor *editor; + EHTMLEditorView *view; ESourceRegistry *registry; - GtkWidget *editor; + GtkWidget *widget; registry = e_mail_signature_manager_get_registry (manager); - editor = e_mail_signature_editor_new (registry, NULL); - gtkhtml_editor_set_html_mode ( - GTKHTML_EDITOR (editor), manager->priv->prefer_html); - mail_signature_manager_emit_editor_created (manager, editor); + widget = e_mail_signature_editor_new (registry, NULL); + + editor = e_mail_signature_editor_get_editor ( + E_MAIL_SIGNATURE_EDITOR (widget)); + view = e_html_editor_get_view (editor); + e_html_editor_view_set_html_mode ( + view, manager->priv->prefer_html); + + mail_signature_manager_emit_editor_created (manager, widget); gtk_widget_grab_focus (manager->priv->tree_view); } @@ -435,6 +442,7 @@ mail_signature_manager_editor_created (EMailSignatureManager *manager, gtk_window_set_transient_for (GTK_WINDOW (editor), parent); gtk_window_set_position (GTK_WINDOW (editor), position); + gtk_widget_set_size_request (GTK_WIDGET (editor), 450, 300); gtk_widget_show (GTK_WIDGET (editor)); } diff --git a/e-util/e-mail-signature-preview.c b/e-util/e-mail-signature-preview.c index c2eeaa6472..ecf64191b6 100644 --- a/e-util/e-mail-signature-preview.c +++ b/e-util/e-mail-signature-preview.c @@ -145,12 +145,16 @@ mail_signature_preview_load_cb (ESource *source, mime_type = e_source_mail_signature_get_mime_type (extension); if (g_strcmp0 (mime_type, "text/html") == 0) { - e_web_view_load_string (E_WEB_VIEW (preview), contents); + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (preview), contents, + "text/html", "UTF-8", "file:///"); } else { gchar *string; string = g_markup_printf_escaped ("<pre>%s</pre>", contents); - e_web_view_load_string (E_WEB_VIEW (preview), string); + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (preview), string, + "text/html", "UTF-8", "file:///"); g_free (string); } diff --git a/e-util/e-misc-utils.c b/e-util/e-misc-utils.c index 510dad39b2..88f6da9599 100644 --- a/e-util/e-misc-utils.c +++ b/e-util/e-misc-utils.c @@ -1089,6 +1089,50 @@ e_str_without_underscores (const gchar *string) return new_string; } +/** + * e_str_replace_string + * @text: the string to replace + * @before: the string to be replaced + * @after: the string to replaced with + * + * Replaces every occurrence of the string @before with the string @after in + * the string @text and returns a #GString with result that should be freed + * with g_string_free(). + * + * Returns: a newly-allocated #GString + */ +GString * +e_str_replace_string (const gchar *text, + const gchar *before, + const gchar *after) +{ + const gchar *p, *next; + GString *str; + gint find_len; + + g_return_val_if_fail (text != NULL, NULL); + g_return_val_if_fail (before != NULL, NULL); + g_return_val_if_fail (*before, NULL); + + find_len = strlen (before); + str = g_string_new (""); + + p = text; + while (next = strstr (p, before), next) { + if (p < next) + g_string_append_len (str, p, next - p); + + if (after && *after) + g_string_append (str, after); + + p = next + find_len; + } + + g_string_append (str, p); + + return str; +} + gint e_str_compare (gconstpointer x, gconstpointer y) diff --git a/e-util/e-misc-utils.h b/e-util/e-misc-utils.h index ebc860b91e..5810490b4f 100644 --- a/e-util/e-misc-utils.h +++ b/e-util/e-misc-utils.h @@ -99,6 +99,9 @@ gchar * e_ascii_dtostr (gchar *buffer, gdouble d); gchar * e_str_without_underscores (const gchar *string); +GString * e_str_replace_string (const gchar *text, + const gchar *find, + const gchar *replace); gint e_str_compare (gconstpointer x, gconstpointer y); gint e_str_case_compare (gconstpointer x, diff --git a/e-util/e-name-selector-entry.c b/e-util/e-name-selector-entry.c index a50c886e9c..5a6519c39b 100644 --- a/e-util/e-name-selector-entry.c +++ b/e-util/e-name-selector-entry.c @@ -511,10 +511,10 @@ is_quoted_at (const gchar *string, gunichar c = g_utf8_get_char (p); if (c == '"') - quoted = ~quoted; + quoted = !quoted; } - return quoted ? TRUE : FALSE; + return quoted; } static gint @@ -530,7 +530,7 @@ get_index_at_position (const gchar *string, gunichar c = g_utf8_get_char (p); if (c == '"') - quoted = ~quoted; + quoted = !quoted; else if (c == ',' && !quoted) n++; } diff --git a/e-util/e-spell-checker.c b/e-util/e-spell-checker.c new file mode 100644 index 0000000000..c781672103 --- /dev/null +++ b/e-util/e-spell-checker.c @@ -0,0 +1,783 @@ +/* + * e-spell-checker.c + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-spell-checker.h" +#include "e-spell-dictionary.h" + +#include <libebackend/libebackend.h> +#include <webkit/webkitspellchecker.h> +#include <pango/pango.h> +#include <gtk/gtk.h> +#include <string.h> + +#define E_SPELL_CHECKER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SPELL_CHECKER, ESpellCheckerPrivate)) + +#define MAX_SUGGESTIONS 10 + +struct _ESpellCheckerPrivate { + EnchantBroker *broker; + GHashTable *active_dictionaries; + GHashTable *dictionaries_cache; + gboolean dictionaries_loaded; + + /* We retain ownership of the EnchantDict's since they + * have to be freed through enchant_broker_free_dict() + * and we also own the EnchantBroker. */ + GHashTable *enchant_dicts; +}; + +enum { + PROP_0, + PROP_ACTIVE_LANGUAGES +}; + +/* Forward Declarations */ +static void e_spell_checker_init_webkit_checker + (WebKitSpellCheckerInterface *interface); + +G_DEFINE_TYPE_EXTENDED ( + ESpellChecker, + e_spell_checker, + G_TYPE_OBJECT, + 0, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL) + G_IMPLEMENT_INTERFACE ( + WEBKIT_TYPE_SPELL_CHECKER, + e_spell_checker_init_webkit_checker)) + +/** + * ESpellChecker: + * + * #ESpellChecker represents a spellchecker in Evolution. It can be used as a + * provider for dictionaries. It also implements #WebKitSpellCheckerInterface, + * so it can be set as a default spell-checker to WebKit editors + */ + +static gboolean +spell_checker_enchant_dicts_foreach_cb (gpointer key, + gpointer value, + gpointer user_data) +{ + EnchantDict *enchant_dict = value; + EnchantBroker *enchant_broker = user_data; + + enchant_broker_free_dict (enchant_broker, enchant_dict); + + return TRUE; +} + +static void +wksc_check_spelling (WebKitSpellChecker *webkit_checker, + const gchar *word, + gint *misspelling_location, + gint *misspelling_length) +{ + ESpellChecker *checker = E_SPELL_CHECKER (webkit_checker); + GHashTable *active_dictionaries; + PangoLanguage *language; + PangoLogAttr *attrs; + gint length, ii; + + active_dictionaries = checker->priv->active_dictionaries; + if (g_hash_table_size (active_dictionaries) == 0) + return; + + length = g_utf8_strlen (word, -1); + + language = pango_language_get_default (); + attrs = g_new (PangoLogAttr, length + 1); + + pango_get_log_attrs (word, -1, -1, language, attrs, length + 1); + + for (ii = 0; ii < length + 1; ii++) { + /* We go through each character until we find an is_word_start, + * then we get into an inner loop to find the is_word_end + * corresponding */ + if (attrs[ii].is_word_start) { + gboolean word_recognized; + gint start = ii; + gint end = ii; + gint word_length; + gchar *cstart; + gint bytes; + gchar *new_word; + + while (attrs[end].is_word_end < 1) + end++; + + word_length = end - start; + /* Set the iterator to be at the current word + * end, so we don't check characters twice. */ + ii = end; + + cstart = g_utf8_offset_to_pointer (word, start); + bytes = g_utf8_offset_to_pointer (word, end) - cstart; + new_word = g_new0 (gchar, bytes + 1); + + g_utf8_strncpy (new_word, cstart, word_length); + + word_recognized = e_spell_checker_check_word ( + checker, new_word, strlen (new_word)); + + if (word_recognized) { + if (misspelling_location != NULL) + *misspelling_location = -1; + if (misspelling_length != NULL) + *misspelling_length = 0; + } else { + if (misspelling_location != NULL) + *misspelling_location = start; + if (misspelling_length != NULL) + *misspelling_length = word_length; + } + + g_free (new_word); + } + } + + g_free (attrs); +} + +static gchar ** +wksc_get_guesses (WebKitSpellChecker *webkit_checker, + const gchar *word, + const gchar *context) +{ + ESpellChecker *checker = E_SPELL_CHECKER (webkit_checker); + GHashTable *active_dictionaries; + GList *list, *link; + gchar ** guesses; + gint ii = 0; + + guesses = g_new0 (gchar *, MAX_SUGGESTIONS + 1); + + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + GList *suggestions; + + dictionary = E_SPELL_DICTIONARY (link->data); + suggestions = e_spell_dictionary_get_suggestions ( + dictionary, word, -1); + + while (suggestions != NULL && ii < MAX_SUGGESTIONS) { + guesses[ii++] = suggestions->data; + suggestions->data = NULL; + + suggestions = g_list_delete_link ( + suggestions, suggestions); + } + + g_list_free_full (suggestions, (GDestroyNotify) g_free); + + if (ii >= MAX_SUGGESTIONS) + break; + } + + g_list_free (list); + + return guesses; +} + +static gchar * +wksc_get_autocorrect_suggestions (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Not supported/needed */ + return NULL; +} + +static void +spell_checker_learn_word (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Carefully, this will add the word to all active dictionaries! */ + + ESpellChecker *checker; + GHashTable *active_dictionaries; + GList *list, *link; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + e_spell_dictionary_learn_word (dictionary, word, -1); + } + + g_list_free (list); +} + +static void +spell_checker_ignore_word (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Carefully, this will add the word to all active dictionaries */ + + ESpellChecker *checker; + GHashTable *active_dictionaries; + GList *list, *link; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + e_spell_dictionary_ignore_word (dictionary, word, -1); + } + + g_list_free (list); +} + +static void +wksc_update_languages (WebKitSpellChecker *webkit_checker, + const gchar *languages) +{ + ESpellChecker *checker; + GHashTable *active_dictionaries; + GQueue queue = G_QUEUE_INIT; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + + if (languages != NULL) { + gchar **langs; + gint ii; + + langs = g_strsplit (languages, ",", -1); + for (ii = 0; langs[ii] != NULL; ii++) { + ESpellDictionary *dictionary; + + dictionary = e_spell_checker_ref_dictionary ( + checker, langs[ii]); + if (dictionary != NULL) + g_queue_push_tail (&queue, dictionary); + } + g_strfreev (langs); + } else { + ESpellDictionary *dictionary; + PangoLanguage *pango_language; + const gchar *language; + + pango_language = gtk_get_default_language (); + language = pango_language_to_string (pango_language); + dictionary = e_spell_checker_ref_dictionary (checker, language); + + if (dictionary == NULL) { + GList *list; + + list = e_spell_checker_list_available_dicts (checker); + if (list != NULL) { + dictionary = g_object_ref (list->data); + g_list_free (list); + } + } + + if (dictionary != NULL) + g_queue_push_tail (&queue, dictionary); + } + + g_hash_table_remove_all (active_dictionaries); + + while (!g_queue_is_empty (&queue)) { + ESpellDictionary *dictionary; + + dictionary = g_queue_pop_head (&queue); + g_hash_table_add (active_dictionaries, dictionary); + } + + g_object_notify (G_OBJECT (checker), "active-languages"); +} + +static void +spell_checker_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_LANGUAGES: + g_value_take_boxed ( + value, + e_spell_checker_list_active_languages ( + E_SPELL_CHECKER (object), NULL)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +spell_checker_dispose (GObject *object) +{ + ESpellCheckerPrivate *priv; + + priv = E_SPELL_CHECKER_GET_PRIVATE (object); + + g_hash_table_remove_all (priv->active_dictionaries); + g_hash_table_remove_all (priv->dictionaries_cache); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->dispose (object); +} + +static void +spell_checker_finalize (GObject *object) +{ + ESpellCheckerPrivate *priv; + + priv = E_SPELL_CHECKER_GET_PRIVATE (object); + + /* Freeing EnchantDicts requires help from EnchantBroker. */ + g_hash_table_foreach_remove ( + priv->enchant_dicts, + spell_checker_enchant_dicts_foreach_cb, + priv->broker); + g_hash_table_destroy (priv->enchant_dicts); + + enchant_broker_free (priv->broker); + + g_hash_table_destroy (priv->active_dictionaries); + g_hash_table_destroy (priv->dictionaries_cache); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->finalize (object); +} + +static void +spell_checker_constructed (GObject *object) +{ + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->constructed (object); + + e_extensible_load_extensions (E_EXTENSIBLE (object)); +} + +static void +e_spell_checker_class_init (ESpellCheckerClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ESpellCheckerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = spell_checker_get_property; + object_class->dispose = spell_checker_dispose; + object_class->finalize = spell_checker_finalize; + object_class->constructed = spell_checker_constructed; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_LANGUAGES, + g_param_spec_boxed ( + "active-languages", + "Active Languages", + "Active spell check language codes", + G_TYPE_STRV, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_spell_checker_init_webkit_checker (WebKitSpellCheckerInterface *interface) +{ + interface->check_spelling_of_string = wksc_check_spelling; + interface->get_autocorrect_suggestions_for_misspelled_word = + wksc_get_autocorrect_suggestions; + interface->get_guesses_for_word = wksc_get_guesses; + interface->ignore_word = spell_checker_ignore_word; + interface->learn_word = spell_checker_learn_word; + interface->update_spell_checking_languages = wksc_update_languages; +} + +static void +e_spell_checker_init (ESpellChecker *checker) +{ + GHashTable *active_dictionaries; + GHashTable *dictionaries_cache; + GHashTable *enchant_dicts; + + active_dictionaries = g_hash_table_new_full ( + (GHashFunc) e_spell_dictionary_hash, + (GEqualFunc) e_spell_dictionary_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) NULL); + + dictionaries_cache = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) NULL, + (GDestroyNotify) g_object_unref); + + enchant_dicts = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + + checker->priv = E_SPELL_CHECKER_GET_PRIVATE (checker); + + checker->priv->broker = enchant_broker_init (); + checker->priv->active_dictionaries = active_dictionaries; + checker->priv->dictionaries_cache = dictionaries_cache; + checker->priv->enchant_dicts = enchant_dicts; +} + +/** + * e_spell_checker_new: + * + * Creates a new #ESpellChecker instance. + * + * Returns: a new #ESpellChecker + **/ +ESpellChecker * +e_spell_checker_new (void) +{ + return g_object_new (E_TYPE_SPELL_CHECKER, NULL); +} + +static void +list_enchant_dicts (const gchar * const lang_tag, + const gchar * const provider_name, + const gchar * const provider_desc, + const gchar * const provider_file, + gpointer user_data) +{ + ESpellChecker *checker = user_data; + EnchantDict *enchant_dict; + + enchant_dict = enchant_broker_request_dict ( + checker->priv->broker, lang_tag); + if (enchant_dict != NULL) { + ESpellDictionary *dictionary; + const gchar *code; + + /* Note that we retain ownership of the EnchantDict. + * Since EnchantDict is not reference counted, we're + * merely loaning the pointer to ESpellDictionary. */ + dictionary = e_spell_dictionary_new (checker, enchant_dict); + code = e_spell_dictionary_get_code (dictionary); + + g_hash_table_insert ( + checker->priv->dictionaries_cache, + (gpointer) code, dictionary); + + g_hash_table_insert ( + checker->priv->enchant_dicts, + g_strdup (code), enchant_dict); + } +} + +/** + * e_spell_checker_list_available_dicts: + * @checker: An #ESpellChecker + * + * Returns list of all dictionaries available to the actual + * spell-checking backend. + * + * Returns: new copy of #GList of #ESpellDictionary. The dictionaries are + * owned by the @checker and should not be free'd. The list should be freed + * using g_list_free() when not neede anymore. [transfer-list] + */ +GList * +e_spell_checker_list_available_dicts (ESpellChecker *checker) +{ + GList *list; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + if (!checker->priv->dictionaries_loaded) { + enchant_broker_list_dicts ( + checker->priv->broker, + list_enchant_dicts, checker); + checker->priv->dictionaries_loaded = TRUE; + } + + list = g_hash_table_get_values (checker->priv->dictionaries_cache); + + return g_list_sort (list, (GCompareFunc) e_spell_dictionary_compare); +} + +/** + * e_spell_checker_ref_dictionary: + * @checker: an #ESpellChecker + * @language_code: (allow-none): language code of a dictionary, or %NULL + * + * Tries to find an #ESpellDictionary for given @language_code. + * If @language_code is %NULL, the function will return a default + * #ESpellDictionary. + * + * Returns: an #ESpellDictionary for @language_code + */ +ESpellDictionary * +e_spell_checker_ref_dictionary (ESpellChecker *checker, + const gchar *language_code) +{ + ESpellDictionary *dictionary; + GList *list; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + /* If the cache has not yet been initialized, do so - we will need + * it anyway, Otherwise is this call very cheap */ + list = e_spell_checker_list_available_dicts (checker); + + if (language_code == NULL) { + dictionary = (list != NULL) ? list->data : NULL; + } else { + dictionary = g_hash_table_lookup ( + checker->priv->dictionaries_cache, + language_code); + } + + if (dictionary != NULL) + g_object_ref (dictionary); + + g_list_free (list); + + return dictionary; +} + +/** + * e_spell_checker_get_enchant_dict: + * @checker: an #ESpellChecker + * @language_code: language code of a dictionary, or %NULL + * + * Returns the #EnchantDict for @language_code, or %NULL if there is none. + * + * Returns: the #EnchantDict for @language_code, or %NULL + **/ +EnchantDict * +e_spell_checker_get_enchant_dict (ESpellChecker *checker, + const gchar *language_code) +{ + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + g_return_val_if_fail (language_code != NULL, NULL); + + return g_hash_table_lookup ( + checker->priv->enchant_dicts, language_code); +} + +gboolean +e_spell_checker_get_language_active (ESpellChecker *checker, + const gchar *language_code) +{ + ESpellDictionary *dictionary; + GHashTable *active_dictionaries; + gboolean active; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), FALSE); + g_return_val_if_fail (language_code != NULL, FALSE); + + dictionary = e_spell_checker_ref_dictionary (checker, language_code); + g_return_val_if_fail (dictionary != NULL, FALSE); + + active_dictionaries = checker->priv->active_dictionaries; + active = g_hash_table_contains (active_dictionaries, dictionary); + + g_object_unref (dictionary); + + return active; +} + +void +e_spell_checker_set_language_active (ESpellChecker *checker, + const gchar *language_code, + gboolean active) +{ + ESpellDictionary *dictionary; + GHashTable *active_dictionaries; + gboolean is_active; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + g_return_if_fail (language_code != NULL); + + dictionary = e_spell_checker_ref_dictionary (checker, language_code); + g_return_if_fail (dictionary != NULL); + + active_dictionaries = checker->priv->active_dictionaries; + is_active = g_hash_table_contains (active_dictionaries, dictionary); + + if (active && !is_active) { + g_object_ref (dictionary); + g_hash_table_add (active_dictionaries, dictionary); + g_object_notify (G_OBJECT (checker), "active-languages"); + } else if (!active && is_active) { + g_hash_table_remove (active_dictionaries, dictionary); + g_object_notify (G_OBJECT (checker), "active-languages"); + } + + g_object_unref (dictionary); +} + +/** + * e_spell_checker_list_active_languages: + * @checker: an #ESpellChecker + * @n_languages: return location for the number of active languages, or %NULL + * + * Returns a %NULL-terminated array of language codes actively being used + * for spell checking. Free the returned array with g_strfreev(). + * + * Returns: a %NULL-teriminated array of language codes + **/ +gchar ** +e_spell_checker_list_active_languages (ESpellChecker *checker, + guint *n_languages) +{ + GHashTable *active_dictionaries; + GList *list, *link; + gchar **active_languages; + guint size; + gint ii = 0; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + size = g_hash_table_size (active_dictionaries); + + active_languages = g_new0 (gchar *, size + 1); + + list = g_list_sort (list, (GCompareFunc) e_spell_dictionary_compare); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + const gchar *language_code; + + dictionary = E_SPELL_DICTIONARY (link->data); + language_code = e_spell_dictionary_get_code (dictionary); + active_languages[ii++] = g_strdup (language_code); + } + + if (n_languages != NULL) + *n_languages = size; + + g_list_free (list); + + return active_languages; +} + +/** + * e_spell_checker_count_active_languages: + * @checker: an #ESpellChecker + * + * Returns the number of languages actively being used for spell checking. + * + * Returns: number of active spell checking languages + **/ +guint +e_spell_checker_count_active_languages (ESpellChecker *checker) +{ + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), 0); + + return g_hash_table_size (checker->priv->active_dictionaries); +} + +/** + * e_spell_checker_check_word: + * @checker: an #SpellChecker + * @word: a word to spell-check + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Calls e_spell_dictionary_check_word() on all active dictionaries in + * @checker, and returns %TRUE if @word is recognized by any of them. + * + * Returns: %TRUE if @word is recognized, %FALSE otherwise + **/ +gboolean +e_spell_checker_check_word (ESpellChecker *checker, + const gchar *word, + gsize length) +{ + GList *list, *link; + gboolean recognized = FALSE; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), TRUE); + g_return_val_if_fail (word != NULL && *word != '\0', TRUE); + + list = g_hash_table_get_keys (checker->priv->active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + if (e_spell_dictionary_check_word (dictionary, word, length)) { + recognized = TRUE; + break; + } + } + + g_list_free (list); + + return recognized; +} + +/** + * e_spell_checker_ignore_word: + * @checker: an #ESpellChecker + * @word: word to ignore for the rest of session + * + * Calls e_spell_dictionary_ignore_word() on all active dictionaries in + * the @checker. + */ +void +e_spell_checker_ignore_word (ESpellChecker *checker, + const gchar *word) +{ + WebKitSpellCheckerInterface *interface; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + + interface = WEBKIT_SPELL_CHECKER_GET_IFACE (checker); + interface->ignore_word (WEBKIT_SPELL_CHECKER (checker), word); +} + +/** + * e_spell_checker_learn_word: + * @checker: an #ESpellChecker + * @word: word to learn + * + * Calls e_spell_dictionary_learn_word() on all active dictionaries in + * the @checker. + */ +void +e_spell_checker_learn_word (ESpellChecker *checker, + const gchar *word) +{ + WebKitSpellCheckerInterface *interface; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + + interface = WEBKIT_SPELL_CHECKER_GET_IFACE (checker); + interface->learn_word (WEBKIT_SPELL_CHECKER (checker), word); +} diff --git a/e-util/e-spell-checker.h b/e-util/e-spell-checker.h new file mode 100644 index 0000000000..48303d66a9 --- /dev/null +++ b/e-util/e-spell-checker.h @@ -0,0 +1,95 @@ +/* + * e-spell-checker.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SPELL_CHECKER_H +#define E_SPELL_CHECKER_H + +#include <glib-object.h> +#include <e-util/e-spell-dictionary.h> + +/* Standard GObject macros */ +#define E_TYPE_SPELL_CHECKER \ + (e_spell_checker_get_type ()) +#define E_SPELL_CHECKER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SPELL_CHECKER, ESpellChecker)) +#define E_SPELL_CHECKER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SPELL_CHECKER, ESpellCheckerClass)) +#define E_IS_SPELL_CHECKER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SPELL_CHECKER)) +#define E_IS_SPELL_CHECKER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SPELL_CHECKER)) +#define E_SPELL_CHECKER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SPELL_CHECKER, ESpellCheckerClass)) + +G_BEGIN_DECLS + +typedef struct _ESpellChecker ESpellChecker; +typedef struct _ESpellCheckerPrivate ESpellCheckerPrivate; +typedef struct _ESpellCheckerClass ESpellCheckerClass; + +struct _ESpellChecker { + GObject parent; + ESpellCheckerPrivate *priv; +}; + +struct _ESpellCheckerClass { + GObjectClass parent_class; +}; + +GType e_spell_checker_get_type (void) G_GNUC_CONST; +ESpellChecker * e_spell_checker_new (void); +GList * e_spell_checker_list_available_dicts + (ESpellChecker *checker); +ESpellDictionary * + e_spell_checker_ref_dictionary (ESpellChecker *checker, + const gchar *language_code); +EnchantDict * e_spell_checker_get_enchant_dict + (ESpellChecker *checker, + const gchar *language_code); +gboolean e_spell_checker_get_language_active + (ESpellChecker *checker, + const gchar *language_code); +void e_spell_checker_set_language_active + (ESpellChecker *checker, + const gchar *language_code, + gboolean active); +gchar ** e_spell_checker_list_active_languages + (ESpellChecker *checker, + guint *n_languages); +guint e_spell_checker_count_active_languages + (ESpellChecker *checker); +gboolean e_spell_checker_check_word (ESpellChecker *checker, + const gchar *word, + gsize length); +void e_spell_checker_learn_word (ESpellChecker *checker, + const gchar *word); +void e_spell_checker_ignore_word (ESpellChecker *checker, + const gchar *word); + +G_END_DECLS + +#endif /* E_SPELL_CHECKER_H */ diff --git a/e-util/e-spell-dictionary.c b/e-util/e-spell-dictionary.c new file mode 100644 index 0000000000..e6e06b7d9a --- /dev/null +++ b/e-util/e-spell-dictionary.c @@ -0,0 +1,797 @@ +/* + * e-spell-dictionary.c + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-spell-dictionary.h" +#include "e-spell-checker.h" + +#include <glib/gi18n-lib.h> +#include <string.h> + +#define E_SPELL_DICTIONARY_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SPELL_DICTIONARY, ESpellDictionaryPrivate)) + +/** + * ESpellDictionary: + * + * The #ESpellDictionary is a wrapper around #EnchantDict. + */ + +enum { + PROP_0, + PROP_SPELL_CHECKER +}; + +struct _ESpellDictionaryPrivate { + GWeakRef spell_checker; + + gchar *name; + gchar *code; + gchar *collate_key; +}; + +#define ISO_639_DOMAIN "iso_639" +#define ISO_3166_DOMAIN "iso_3166" + +static GHashTable *iso_639_table = NULL; +static GHashTable *iso_3166_table = NULL; + +G_DEFINE_TYPE ( + ESpellDictionary, + e_spell_dictionary, + G_TYPE_OBJECT); + +#ifdef HAVE_ISO_CODES + +#define ISOCODESLOCALEDIR ISO_CODES_PREFIX "/share/locale" + +#ifdef G_OS_WIN32 +#ifdef DATADIR +#undef DATADIR +#endif +#include <shlobj.h> +static HMODULE hmodule; + +BOOL WINAPI +DllMain (HINSTANCE hinstDLL, + DWORD fdwReason, + LPVOID lpvReserved); + +BOOL WINAPI +DllMain (HINSTANCE hinstDLL, + DWORD fdwReason, + LPVOID lpvReserved) +{ + switch (fdwReason) + { + case DLL_PROCESS_ATTACH: + hmodule = hinstDLL; + break; + } + + return TRUE; +} + +static gchar * +_get_iso_codes_prefix (void) +{ + static gchar retval[1000]; + static gint beenhere = 0; + gchar *temp_dir = 0; + + if (beenhere) + return retval; + + if (!(temp_dir = g_win32_get_package_installation_directory_of_module ((gpointer) hmodule))) { + strcpy (retval, ISO_CODES_PREFIX); + return retval; + } + + strcpy (retval, temp_dir); + g_free (temp_dir); + beenhere = 1; + return retval; +} + +static gchar * +_get_isocodeslocaledir (void) +{ + static gchar retval[1000]; + static gint beenhere = 0; + + if (beenhere) + return retval; + + strcpy (retval, _get_iso_codes_prefix ()); + strcat (retval, "\\share\\locale" ); + beenhere = 1; + return retval; +} + +#undef ISO_CODES_PREFIX +#define ISO_CODES_PREFIX _get_iso_codes_prefix () + +#undef ISOCODESLOCALEDIR +#define ISOCODESLOCALEDIR _get_isocodeslocaledir () + +#endif + +static void +iso_639_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer data, + GError **error) +{ + GHashTable *hash_table = data; + const gchar *iso_639_1_code = NULL; + const gchar *iso_639_2_code = NULL; + const gchar *name = NULL; + const gchar *code = NULL; + gint ii; + + if (g_strcmp0 (element_name, "iso_639_entry") != 0) { + return; + } + + for (ii = 0; attribute_names[ii] != NULL; ii++) { + if (strcmp (attribute_names[ii], "name") == 0) + name = attribute_values[ii]; + else if (strcmp (attribute_names[ii], "iso_639_1_code") == 0) + iso_639_1_code = attribute_values[ii]; + else if (strcmp (attribute_names[ii], "iso_639_2T_code") == 0) + iso_639_2_code = attribute_values[ii]; + } + + code = (iso_639_1_code != NULL) ? iso_639_1_code : iso_639_2_code; + + if (code != NULL && *code != '\0' && name != NULL && *name != '\0') + g_hash_table_insert ( + hash_table, g_strdup (code), + g_strdup (dgettext (ISO_639_DOMAIN, name))); +} + +static void +iso_3166_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer data, + GError **error) +{ + GHashTable *hash_table = data; + const gchar *name = NULL; + const gchar *code = NULL; + gint ii; + + if (strcmp (element_name, "iso_3166_entry") != 0) + return; + + for (ii = 0; attribute_names[ii] != NULL; ii++) { + if (strcmp (attribute_names[ii], "name") == 0) + name = attribute_values[ii]; + else if (strcmp (attribute_names[ii], "alpha_2_code") == 0) + code = attribute_values[ii]; + } + + if (code != NULL && *code != '\0' && name != NULL && *name != '\0') + g_hash_table_insert ( + hash_table, g_ascii_strdown (code, -1), + g_strdup (dgettext (ISO_3166_DOMAIN, name))); +} + +static GMarkupParser iso_639_parser = { + iso_639_start_element, + NULL, NULL, NULL, NULL +}; + +static GMarkupParser iso_3166_parser = { + iso_3166_start_element, + NULL, NULL, NULL, NULL +}; + +static void +iso_codes_parse (const GMarkupParser *parser, + const gchar *basename, + GHashTable *hash_table) +{ + GMappedFile *mapped_file; + gchar *filename; + GError *error = NULL; + + filename = g_build_filename ( + ISO_CODES_PREFIX, "share", "xml", + "iso-codes", basename, NULL); + mapped_file = g_mapped_file_new (filename, FALSE, &error); + g_free (filename); + + if (mapped_file != NULL) { + GMarkupParseContext *context; + const gchar *contents; + gsize length; + + context = g_markup_parse_context_new ( + parser, 0, hash_table, NULL); + contents = g_mapped_file_get_contents (mapped_file); + length = g_mapped_file_get_length (mapped_file); + g_markup_parse_context_parse ( + context, contents, length, &error); + g_markup_parse_context_free (context); +#if GLIB_CHECK_VERSION(2,21,3) + g_mapped_file_unref (mapped_file); +#else + g_mapped_file_free (mapped_file); +#endif + } + + if (error != NULL) { + g_warning ("%s: %s", basename, error->message); + g_error_free (error); + } +} + +#endif /* HAVE_ISO_CODES */ + +struct _enchant_dict_description_data { + gchar *language_tag; + gchar *dict_name; +}; + +static void +describe_dictionary (const gchar *language_tag, + const gchar *provider_name, + const gchar *provider_desc, + const gchar *provider_file, + gpointer user_data) +{ + struct _enchant_dict_description_data *data = user_data; + const gchar *iso_639_name; + const gchar *iso_3166_name; + gchar *language_name; + gchar *lowercase; + gchar **tokens; + + /* Split language code into lowercase tokens. */ + lowercase = g_ascii_strdown (language_tag, -1); + tokens = g_strsplit (lowercase, "_", -1); + g_free (lowercase); + + g_return_if_fail (tokens != NULL); + + iso_639_name = g_hash_table_lookup (iso_639_table, tokens[0]); + + if (iso_639_name == NULL) { + language_name = g_strdup_printf ( + /* Translators: %s is the language ISO code. */ + C_("language", "Unknown (%s)"), language_tag); + goto exit; + } + + if (g_strv_length (tokens) < 2) { + language_name = g_strdup (iso_639_name); + goto exit; + } + + iso_3166_name = g_hash_table_lookup (iso_3166_table, tokens[1]); + + if (iso_3166_name != NULL) + language_name = g_strdup_printf ( + /* Translators: The first %s is the language name, and the + * second is the country name. Example: "French (France)" */ + C_("language", "%s (%s)"), iso_639_name, iso_3166_name); + else + language_name = g_strdup_printf ( + /* Translators: The first %s is the language name, and the + * second is the country name. Example: "French (France)" */ + C_("language", "%s (%s)"), iso_639_name, tokens[1]); + +exit: + g_strfreev (tokens); + + data->language_tag = g_strdup (language_tag); + data->dict_name = language_name; +} + +static void +spell_dictionary_set_enchant_dict (ESpellDictionary *dictionary, + EnchantDict *enchant_dict) +{ + struct _enchant_dict_description_data data; + + enchant_dict_describe (enchant_dict, describe_dictionary, &data); + + dictionary->priv->code = data.language_tag; + dictionary->priv->name = data.dict_name; + dictionary->priv->collate_key = g_utf8_collate_key (data.dict_name, -1); +} + +static void +spell_dictionary_set_spell_checker (ESpellDictionary *dictionary, + ESpellChecker *spell_checker) +{ + g_return_if_fail (E_IS_SPELL_CHECKER (spell_checker)); + + g_weak_ref_set (&dictionary->priv->spell_checker, spell_checker); +} + +static void +spell_dictionary_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SPELL_CHECKER: + spell_dictionary_set_spell_checker ( + E_SPELL_DICTIONARY (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +spell_dictionary_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SPELL_CHECKER: + g_value_take_object ( + value, + e_spell_dictionary_ref_spell_checker ( + E_SPELL_DICTIONARY (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +spell_dictionary_dispose (GObject *object) +{ + ESpellDictionaryPrivate *priv; + + priv = E_SPELL_DICTIONARY_GET_PRIVATE (object); + + g_weak_ref_set (&priv->spell_checker, NULL); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_spell_dictionary_parent_class)->dispose (object); +} + +static void +spell_dictionary_finalize (GObject *object) +{ + ESpellDictionaryPrivate *priv; + + priv = E_SPELL_DICTIONARY_GET_PRIVATE (object); + + g_free (priv->name); + g_free (priv->code); + g_free (priv->collate_key); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_spell_dictionary_parent_class)->finalize (object); +} + +static void +e_spell_dictionary_class_init (ESpellDictionaryClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ESpellDictionaryPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = spell_dictionary_set_property; + object_class->get_property = spell_dictionary_get_property; + object_class->dispose = spell_dictionary_dispose; + object_class->finalize = spell_dictionary_finalize; + + g_object_class_install_property ( + object_class, + PROP_SPELL_CHECKER, + g_param_spec_object ( + "spell-checker", + NULL, + "Parent spell checker", + E_TYPE_SPELL_CHECKER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); +} + +static void +e_spell_dictionary_init (ESpellDictionary *dictionary) +{ + dictionary->priv = E_SPELL_DICTIONARY_GET_PRIVATE (dictionary); + + if (!iso_639_table && !iso_3166_table) { +#if defined (ENABLE_NLS) && defined (HAVE_ISO_CODES) + bindtextdomain (ISO_639_DOMAIN, ISOCODESLOCALEDIR); + bind_textdomain_codeset (ISO_639_DOMAIN, "UTF-8"); + + bindtextdomain (ISO_3166_DOMAIN, ISOCODESLOCALEDIR); + bind_textdomain_codeset (ISO_3166_DOMAIN, "UTF-8"); +#endif /* ENABLE_NLS && HAVE_ISO_CODES */ + + iso_639_table = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + iso_3166_table = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + +#ifdef HAVE_ISO_CODES + iso_codes_parse ( + &iso_639_parser, "iso_639.xml", iso_639_table); + iso_codes_parse ( + &iso_3166_parser, "iso_3166.xml", iso_3166_table); +#endif /* HAVE_ISO_CODES */ + } +} + +ESpellDictionary * +e_spell_dictionary_new (ESpellChecker *spell_checker, + EnchantDict *enchant_dict) +{ + ESpellDictionary *dictionary; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (spell_checker), NULL); + g_return_val_if_fail (enchant_dict != NULL, NULL); + + dictionary = g_object_new ( + E_TYPE_SPELL_DICTIONARY, + "spell-checker", spell_checker, NULL); + + /* Since EnchantDict is not reference counted, ESpellChecker + * is loaning us the EnchantDict pointer. We do not own it. */ + spell_dictionary_set_enchant_dict (dictionary, enchant_dict); + + return dictionary; +} + +/** + * e_spell_dictionary_hash: + * @dictionary: an #ESpellDictionary + * + * Generates a hash value for @dictionary based on its ISO code. + * This function is intended for easily hashing an #ESpellDictionary + * to add to a #GHashTable or similar data structure. + * + * Returns: a hash value for @dictionary + **/ +guint +e_spell_dictionary_hash (ESpellDictionary *dictionary) +{ + const gchar *code; + + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), 0); + + code = e_spell_dictionary_get_code (dictionary); + + return g_str_hash (code); +} + +/** + * e_spell_dictionary_equal: + * @dictionary1: an #ESpellDictionary + * @dictionary2: another #ESpellDictionary + * + * Checks two #ESpellDictionary instances for equality based on their + * ISO codes. + * + * Returns: %TRUE if @dictionary1 and @dictionary2 are equal + **/ +gboolean +e_spell_dictionary_equal (ESpellDictionary *dictionary1, + ESpellDictionary *dictionary2) +{ + const gchar *code1, *code2; + + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary1), FALSE); + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary2), FALSE); + + if (dictionary1 == dictionary2) + return TRUE; + + code1 = e_spell_dictionary_get_code (dictionary1); + code2 = e_spell_dictionary_get_code (dictionary2); + + return g_str_equal (code1, code2); +} + +/** + * e_spell_dictionary_compare: + * @dictionary1: an #ESpellDictionary + * @dictionary2: another #ESpellDictionary + * + * Compares @dictionary1 and @dictionary2 by their display names for + * the purpose of lexicographical sorting. Use this function where a + * #GCompareFunc callback is required, such as g_list_sort(). + * + * Returns: 0 if the names match, + * a negative value if @dictionary1 < @dictionary2, + * or a positive value of @dictionary1 > @dictionary2 + **/ +gint +e_spell_dictionary_compare (ESpellDictionary *dictionary1, + ESpellDictionary *dictionary2) +{ + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary1), 0); + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary2), 0); + + return strcmp ( + dictionary1->priv->collate_key, + dictionary2->priv->collate_key); +} + +/** + * e_spell_dictionary_get_name: + * @dictionary: an #ESpellDictionary + * + * Returns the display name of the dictionary (for example + * "English (British)") + * + * Returns: the display name of the @dictionary + */ +const gchar * +e_spell_dictionary_get_name (ESpellDictionary *dictionary) +{ + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), NULL); + + return dictionary->priv->name; +} + +/** + * e_spell_dictionary_get_code: + * @dictionary: an #ESpellDictionary + * + * Returns the ISO code of the spell-checking language for + * @dictionary (for example "en_US"). + * + * Returns: the language code of the @dictionary + */ +const gchar * +e_spell_dictionary_get_code (ESpellDictionary *dictionary) +{ + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), NULL); + + return dictionary->priv->code; +} + +/** + * e_spell_dictionary_ref_spell_checker: + * @dictionary: an #ESpellDictionary + * + * Returns a new reference to the #ESpellChecker which owns the dictionary. + * Unreference the #ESpellChecker with g_object_unref() when finished with it. + * + * Returns: an #ESpellChecker + **/ +ESpellChecker * +e_spell_dictionary_ref_spell_checker (ESpellDictionary *dictionary) +{ + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), NULL); + + return g_weak_ref_get (&dictionary->priv->spell_checker); +} + +/** + * e_spell_dictionary_check_word: + * @dictionary: an #ESpellDictionary + * @word: a word to spell-check + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Tries to lookup the @word in the @dictionary to check whether + * it's spelled correctly or not. + * + * Returns: %TRUE if @word is recognized, %FALSE otherwise + */ +gboolean +e_spell_dictionary_check_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length) +{ + ESpellChecker *spell_checker; + EnchantDict *enchant_dict; + gboolean recognized; + + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), TRUE); + g_return_val_if_fail (word != NULL && *word != '\0', TRUE); + + spell_checker = e_spell_dictionary_ref_spell_checker (dictionary); + g_return_val_if_fail (spell_checker != NULL, TRUE); + + enchant_dict = e_spell_checker_get_enchant_dict ( + spell_checker, e_spell_dictionary_get_code (dictionary)); + g_return_val_if_fail (enchant_dict != NULL, TRUE); + + recognized = (enchant_dict_check (enchant_dict, word, length) == 0); + + g_object_unref (spell_checker); + + return recognized; +} + +/** + * e_spell_dictionary_learn_word: + * @dictionary: an #ESpellDictionary + * @word: a word to add to @dictionary + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Permanently adds @word to @dictionary so that next time calling + * e_spell_dictionary_check() on the @word will return %TRUE. + */ +void +e_spell_dictionary_learn_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length) +{ + ESpellChecker *spell_checker; + EnchantDict *enchant_dict; + + g_return_if_fail (E_IS_SPELL_DICTIONARY (dictionary)); + g_return_if_fail (word != NULL && *word != '\0'); + + spell_checker = e_spell_dictionary_ref_spell_checker (dictionary); + g_return_if_fail (spell_checker != NULL); + + enchant_dict = e_spell_checker_get_enchant_dict ( + spell_checker, e_spell_dictionary_get_code (dictionary)); + g_return_if_fail (enchant_dict != NULL); + + enchant_dict_add_to_personal (enchant_dict, word, length); + + g_object_unref (spell_checker); +} + +/** + * e_spell_dictionary_ignore_word: + * @dictionary: an #ESpellDictionary + * @word: a word to add to ignore list + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Adds @word to temporary ignore list of the @dictionary, so that + * e_spell_dictionary_check() on the @word will return %TRUE. The + * list is cleared when the dictionary is freed. + */ +void +e_spell_dictionary_ignore_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length) +{ + ESpellChecker *spell_checker; + EnchantDict *enchant_dict; + + g_return_if_fail (E_IS_SPELL_DICTIONARY (dictionary)); + g_return_if_fail (word != NULL && *word != '\0'); + + spell_checker = e_spell_dictionary_ref_spell_checker (dictionary); + g_return_if_fail (spell_checker != NULL); + + enchant_dict = e_spell_checker_get_enchant_dict ( + spell_checker, e_spell_dictionary_get_code (dictionary)); + g_return_if_fail (enchant_dict != NULL); + + enchant_dict_add_to_session (enchant_dict, word, length); + + g_object_unref (spell_checker); +} + +/** + * e_spell_dictionary_get_suggestions: + * @dictionary: an #ESpellDictionary + * @word: a word to which to find suggestions + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Provides list of alternative spellings of @word. + * + * Free the returned spelling suggestions with g_free(), and the list + * itself with g_list_free(). An easy way to free the list properly in + * one step is as follows: + * + * |[ + * g_list_free_full (list, (GDestroyNotify) g_free); + * ]| + * + * Returns: a list of spelling suggestions for @word + */ +GList * +e_spell_dictionary_get_suggestions (ESpellDictionary *dictionary, + const gchar *word, + gsize length) +{ + ESpellChecker *spell_checker; + EnchantDict *enchant_dict; + GList *list = NULL; + gchar **suggestions; + gsize ii, count = 0; + + g_return_val_if_fail (E_IS_SPELL_DICTIONARY (dictionary), NULL); + g_return_val_if_fail (word != NULL && *word != '\0', NULL); + + spell_checker = e_spell_dictionary_ref_spell_checker (dictionary); + g_return_val_if_fail (spell_checker != NULL, NULL); + + enchant_dict = e_spell_checker_get_enchant_dict ( + spell_checker, e_spell_dictionary_get_code (dictionary)); + g_return_val_if_fail (enchant_dict != NULL, NULL); + + suggestions = enchant_dict_suggest (enchant_dict, word, length, &count); + for (ii = 0; ii < count; ii++) + list = g_list_prepend (list, g_strdup (suggestions[ii])); + enchant_dict_free_suggestions (enchant_dict, suggestions); + + g_object_unref (spell_checker); + + return g_list_reverse (list); +} + +/** + * e_spell_dictionary_add_correction + * @dictionary: an #ESpellDictionary + * @misspelled: a misspelled word + * @misspelled_length: length of @misspelled in bytes or -1 when + * %NULL-terminated + * @correction: the corrected word + * @correction_length: length of @correction in bytes or -1 when + * %NULL-terminated + * + * Learns a new @correction of @misspelled word. + */ +void +e_spell_dictionary_store_correction (ESpellDictionary *dictionary, + const gchar *misspelled, + gsize misspelled_length, + const gchar *correction, + gsize correction_length) +{ + ESpellChecker *spell_checker; + EnchantDict *enchant_dict; + + g_return_if_fail (E_IS_SPELL_DICTIONARY (dictionary)); + g_return_if_fail (misspelled != NULL && *misspelled != '\0'); + g_return_if_fail (correction != NULL && *correction != '\0'); + + spell_checker = e_spell_dictionary_ref_spell_checker (dictionary); + g_return_if_fail (spell_checker != NULL); + + enchant_dict = e_spell_checker_get_enchant_dict ( + spell_checker, e_spell_dictionary_get_code (dictionary)); + g_return_if_fail (enchant_dict != NULL); + + enchant_dict_store_replacement ( + enchant_dict, + misspelled, misspelled_length, + correction, correction_length); + + g_object_unref (spell_checker); +} + diff --git a/e-util/e-spell-dictionary.h b/e-util/e-spell-dictionary.h new file mode 100644 index 0000000000..f36bfb4704 --- /dev/null +++ b/e-util/e-spell-dictionary.h @@ -0,0 +1,99 @@ +/* + * e-spell-dictionary.h + * + * 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. + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only <e-util/e-util.h> should be included directly." +#endif + +#ifndef E_SPELL_DICTIONARY_H +#define E_SPELL_DICTIONARY_H + +#include <glib-object.h> +#include <enchant/enchant.h> + +/* Standard GObject macros */ +#define E_TYPE_SPELL_DICTIONARY \ + (e_spell_dictionary_get_type ()) +#define E_SPELL_DICTIONARY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SPELL_DICTIONARY, ESpellDictionary)) +#define E_SPELL_DICTIONARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SPELL_DICTIONARY, ESpellDictionaryClass)) +#define E_IS_SPELL_DICTIONARY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SPELL_DICTIONARY)) +#define E_IS_SPELL_DICTIONARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SPELL_DICTIONARY)) +#define E_SPELL_DICTIONARY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SPELL_DICTIONARY, ESpellDictionaryClass)) + +G_BEGIN_DECLS + +typedef struct _ESpellDictionary ESpellDictionary; +typedef struct _ESpellDictionaryPrivate ESpellDictionaryPrivate; +typedef struct _ESpellDictionaryClass ESpellDictionaryClass; +typedef struct _ESpellChecker ESpellChecker; + +struct _ESpellDictionary { + GObject parent; + ESpellDictionaryPrivate *priv; +}; + +struct _ESpellDictionaryClass { + GObjectClass parent_class; +}; + +GType e_spell_dictionary_get_type (void) G_GNUC_CONST; +ESpellDictionary * + e_spell_dictionary_new (ESpellChecker *spell_checker, + EnchantDict *enchant_dict); +guint e_spell_dictionary_hash (ESpellDictionary *dictionary); +gboolean e_spell_dictionary_equal (ESpellDictionary *dictionary1, + ESpellDictionary *dictionary2); +gint e_spell_dictionary_compare (ESpellDictionary *dictionary1, + ESpellDictionary *dictionary2); +const gchar * e_spell_dictionary_get_name (ESpellDictionary *dictionary); +const gchar * e_spell_dictionary_get_code (ESpellDictionary *dictionary); +ESpellChecker * e_spell_dictionary_ref_spell_checker + (ESpellDictionary *dictionary); +gboolean e_spell_dictionary_check_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length); +void e_spell_dictionary_learn_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length); +void e_spell_dictionary_ignore_word (ESpellDictionary *dictionary, + const gchar *word, + gsize length); +GList * e_spell_dictionary_get_suggestions + (ESpellDictionary *dictionary, + const gchar *word, + gsize length); +void e_spell_dictionary_store_correction + (ESpellDictionary *dictionary, + const gchar *misspelled, + gsize misspelled_length, + const gchar *correction, + gsize correction_length); + +G_END_DECLS + +#endif /* E_SPELL_DICTIONARY_H */ diff --git a/e-util/e-spell-entry.c b/e-util/e-spell-entry.c index 75c7a6a9c8..4993612a7a 100644 --- a/e-util/e-spell-entry.c +++ b/e-util/e-spell-entry.c @@ -23,8 +23,7 @@ #include <libebackend/libebackend.h> -#include <editor/gtkhtml-spell-language.h> -#include <editor/gtkhtml-spell-checker.h> +#include <e-util/e-spell-checker.h> #include "e-misc-utils.h" #include "e-spell-entry.h" @@ -37,18 +36,20 @@ struct _ESpellEntryPrivate { PangoAttrList *attr_list; gint mark_character; gint entry_scroll_offset; - GSettings *settings; gboolean custom_checkers; gboolean checking_enabled; - GSList *checkers; gchar **words; gint *word_starts; gint *word_ends; + + ESpellChecker *spell_checker; + guint active_languages_handler_id; }; enum { PROP_0, - PROP_CHECKING_ENABLED + PROP_CHECKING_ENABLED, + PROP_SPELL_CHECKER }; G_DEFINE_TYPE_WITH_CODE ( @@ -76,16 +77,12 @@ word_misspelled (ESpellEntry *entry, g_strlcpy (word, text + start, end - start + 1); if (g_unichar_isalpha (*word)) { - GSList *li; + ESpellChecker *spell_checker; gssize wlen = strlen (word); - for (li = entry->priv->checkers; li; li = g_slist_next (li)) { - GtkhtmlSpellChecker *checker = li->data; - if (gtkhtml_spell_checker_check_word (checker, word, wlen)) { - result = FALSE; - break; - } - } + spell_checker = e_spell_entry_get_spell_checker (entry); + if (e_spell_checker_check_word (spell_checker, word, wlen)) + result = FALSE; } g_free (word); @@ -160,8 +157,13 @@ spell_entry_recheck_all (ESpellEntry *entry) pango_attr_list_unref (entry->priv->attr_list); entry->priv->attr_list = pango_attr_list_new (); - if (e_spell_entry_get_checking_enabled (entry)) - check_words = (entry->priv->checkers != NULL); + if (e_spell_entry_get_checking_enabled (entry)) { + ESpellChecker *spell_checker; + + spell_checker = e_spell_entry_get_spell_checker (entry); + if (e_spell_checker_count_active_languages (spell_checker) > 0) + check_words = TRUE; + } if (check_words) { /* Loop through words */ @@ -269,15 +271,15 @@ add_to_dictionary (GtkWidget *menuitem, { gchar *word; gint start, end; - GtkhtmlSpellChecker *checker; + ESpellDictionary *dict; get_word_extents_from_position ( entry, &start, &end, entry->priv->mark_character); word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); - checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); - if (checker != NULL) - gtkhtml_spell_checker_add_word (checker, word, -1); + dict = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); + if (dict != NULL) + e_spell_dictionary_learn_word (dict, word, -1); g_free (word); @@ -300,18 +302,16 @@ static void ignore_all (GtkWidget *menuitem, ESpellEntry *entry) { + ESpellChecker *spell_checker; gchar *word; gint start, end; - GSList *li; get_word_extents_from_position ( entry, &start, &end, entry->priv->mark_character); word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end); - for (li = entry->priv->checkers; li; li = g_slist_next (li)) { - GtkhtmlSpellChecker *checker = li->data; - gtkhtml_spell_checker_add_word_to_session (checker, word, -1); - } + spell_checker = e_spell_entry_get_spell_checker (entry); + e_spell_checker_ignore_word (spell_checker, word); g_free (word); @@ -338,7 +338,7 @@ replace_word (GtkWidget *menuitem, const gchar *newword; gint start, end; gint cursor; - GtkhtmlSpellChecker *checker; + ESpellDictionary *dict; get_word_extents_from_position ( entry, &start, &end, entry->priv->mark_character); @@ -359,11 +359,11 @@ replace_word (GtkWidget *menuitem, GTK_EDITABLE (entry), newword, strlen (newword), &start); gtk_editable_set_position (GTK_EDITABLE (entry), cursor); - checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); + dict = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker"); - if (checker != NULL) - gtkhtml_spell_checker_store_replacement ( - checker, oldword, -1, newword, -1); + if (dict != NULL) + e_spell_dictionary_store_correction ( + dict, oldword, -1, newword, -1); g_free (oldword); } @@ -371,13 +371,13 @@ replace_word (GtkWidget *menuitem, static void build_suggestion_menu (ESpellEntry *entry, GtkWidget *menu, - GtkhtmlSpellChecker *checker, + ESpellDictionary *dict, const gchar *word) { GtkWidget *mi; GList *suggestions, *iter; - suggestions = gtkhtml_spell_checker_get_suggestions (checker, word, -1); + suggestions = e_spell_dictionary_get_suggestions (dict, word, -1); if (suggestions == NULL) { /* no suggestions. Put something in the menu anyway... */ @@ -414,7 +414,7 @@ build_suggestion_menu (ESpellEntry *entry, } mi = gtk_menu_item_new_with_label (iter->data); - g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker); + g_object_set_data (G_OBJECT (mi), "spell-entry-checker", dict); g_signal_connect (mi, "activate", G_CALLBACK (replace_word), entry); gtk_widget_show (mi); gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi); @@ -428,35 +428,49 @@ static GtkWidget * build_spelling_menu (ESpellEntry *entry, const gchar *word) { - GtkhtmlSpellChecker *checker; + ESpellChecker *spell_checker; + ESpellDictionary *dict; GtkWidget *topmenu, *mi; + GQueue queue = G_QUEUE_INIT; + gchar **active_languages; + guint ii, n_active_languages; gchar *label; topmenu = gtk_menu_new (); - if (entry->priv->checkers == NULL) - return topmenu; + spell_checker = e_spell_entry_get_spell_checker (entry); + + active_languages = e_spell_checker_list_active_languages ( + spell_checker, &n_active_languages); + for (ii = 0; ii < n_active_languages; ii++) { + dict = e_spell_checker_ref_dictionary ( + spell_checker, active_languages[ii]); + if (dict != NULL) + g_queue_push_tail (&queue, dict); + } + g_strfreev (active_languages); + + if (g_queue_is_empty (&queue)) + goto exit; /* Suggestions */ - if (entry->priv->checkers->next == NULL) { - checker = entry->priv->checkers->data; - build_suggestion_menu (entry, topmenu, checker, word); + if (n_active_languages == 1) { + dict = g_queue_peek_head (&queue); + build_suggestion_menu (entry, topmenu, dict, word); } else { - GSList *li; GtkWidget *menu; - const gchar *lang_name; + GList *list, *link; - for (li = entry->priv->checkers; li; li = g_slist_next (li)) { - const GtkhtmlSpellLanguage *language; + list = g_queue_peek_head_link (&queue); - checker = li->data; - language = gtkhtml_spell_checker_get_language (checker); - if (language == NULL) - continue; + for (link = list; link != NULL; link = g_list_next (link)) { + const gchar *lang_name; - lang_name = gtkhtml_spell_language_get_name (language); + dict = E_SPELL_DICTIONARY (link->data); + + lang_name = e_spell_dictionary_get_name (dict); if (lang_name == NULL) - lang_name = gtkhtml_spell_language_get_code (language); + lang_name = e_spell_dictionary_get_code (dict); if (lang_name == NULL) lang_name = "???"; @@ -466,7 +480,7 @@ build_spelling_menu (ESpellEntry *entry, gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); menu = gtk_menu_new (); gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu); - build_suggestion_menu (entry, menu, checker, word); + build_suggestion_menu (entry, menu, dict, word); } } @@ -484,36 +498,34 @@ build_spelling_menu (ESpellEntry *entry, GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_icon_name ("list-add", GTK_ICON_SIZE_MENU)); - if (entry->priv->checkers->next == NULL) { - checker = entry->priv->checkers->data; - g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker); + if (n_active_languages == 1) { + dict = g_queue_peek_head (&queue); + g_object_set_data (G_OBJECT (mi), "spell-entry-checker", dict); g_signal_connect ( mi, "activate", G_CALLBACK (add_to_dictionary), entry); } else { - GSList *li; GtkWidget *menu, *submi; - const gchar *lang_name; + GList *list, *link; menu = gtk_menu_new (); gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu); - for (li = entry->priv->checkers; li; li = g_slist_next (li)) { - const GtkhtmlSpellLanguage *language; + list = g_queue_peek_head_link (&queue); - checker = li->data; - language = gtkhtml_spell_checker_get_language (checker); - if (language == NULL) - continue; + for (link = list; link != NULL; link = g_list_next (link)) { + const gchar *lang_name; + + dict = E_SPELL_DICTIONARY (link->data); - lang_name = gtkhtml_spell_language_get_name (language); + lang_name = e_spell_dictionary_get_name (dict); if (lang_name == NULL) - lang_name = gtkhtml_spell_language_get_code (language); + lang_name = e_spell_dictionary_get_code (dict); if (lang_name == NULL) lang_name = "???"; submi = gtk_menu_item_new_with_label (lang_name); - g_object_set_data (G_OBJECT (submi), "spell-entry-checker", checker); + g_object_set_data (G_OBJECT (submi), "spell-entry-checker", dict); g_signal_connect ( submi, "activate", G_CALLBACK (add_to_dictionary), entry); @@ -535,6 +547,10 @@ build_spelling_menu (ESpellEntry *entry, gtk_widget_show_all (mi); gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi); +exit: + while (!g_queue_is_empty (&queue)) + g_object_unref (g_queue_pop_head (&queue)); + return topmenu; } @@ -580,10 +596,12 @@ spell_entry_populate_popup (ESpellEntry *entry, GtkMenu *menu, gpointer data) { + ESpellChecker *spell_checker; gint start, end; gchar *word; - if (entry->priv->checkers == NULL) + spell_checker = e_spell_entry_get_spell_checker (entry); + if (e_spell_checker_count_active_languages (spell_checker) == 0) return; get_word_extents_from_position ( @@ -606,8 +624,10 @@ static void spell_entry_changed (GtkEditable *editable) { ESpellEntry *entry = E_SPELL_ENTRY (editable); + ESpellChecker *spell_checker; - if (entry->priv->checkers == NULL) + spell_checker = e_spell_entry_get_spell_checker (entry); + if (e_spell_checker_count_active_languages (spell_checker) == 0) return; if (entry->priv->words != NULL) { @@ -633,70 +653,6 @@ spell_entry_notify_scroll_offset (ESpellEntry *spell_entry) &spell_entry->priv->entry_scroll_offset, NULL); } -static GList * -spell_entry_load_spell_languages (void) -{ - GSettings *settings; - GList *spell_languages = NULL; - gchar **strv; - gint ii; - - /* Ask GSettings for a list of spell check language codes. */ - settings = g_settings_new ("org.gnome.evolution.mail"); - strv = g_settings_get_strv (settings, "composer-spell-languages"); - g_object_unref (settings); - - /* Convert the codes to spell language structs. */ - for (ii = 0; strv[ii] != NULL; ii++) { - gchar *language_code = strv[ii]; - const GtkhtmlSpellLanguage *language; - - language = gtkhtml_spell_language_lookup (language_code); - if (language != NULL) - spell_languages = g_list_prepend ( - spell_languages, (gpointer) language); - } - - g_strfreev (strv); - - spell_languages = g_list_reverse (spell_languages); - - /* Pick a default spell language if it came back empty. */ - if (spell_languages == NULL) { - const GtkhtmlSpellLanguage *language; - - language = gtkhtml_spell_language_lookup (NULL); - - if (language) { - spell_languages = g_list_prepend ( - spell_languages, (gpointer) language); - } - } - - return spell_languages; -} - -static void -spell_entry_settings_changed (ESpellEntry *spell_entry, - const gchar *key) -{ - GList *languages; - - g_return_if_fail (spell_entry != NULL); - - if (spell_entry->priv->custom_checkers) - return; - - if (key && !g_str_equal (key, "composer-spell-languages")) - return; - - languages = spell_entry_load_spell_languages (); - e_spell_entry_set_languages (spell_entry, languages); - g_list_free (languages); - - spell_entry->priv->custom_checkers = FALSE; -} - static gint spell_entry_find_position (ESpellEntry *spell_entry, gint x) @@ -722,6 +678,15 @@ spell_entry_find_position (ESpellEntry *spell_entry, } static void +spell_entry_active_languages_cb (ESpellChecker *spell_checker, + GParamSpec *pspec, + ESpellEntry *spell_entry) +{ + if (gtk_widget_get_realized (GTK_WIDGET (spell_entry))) + spell_entry_recheck_all (spell_entry); +} + +static void spell_entry_set_property (GObject *object, guint property_id, const GValue *value, @@ -733,6 +698,12 @@ spell_entry_set_property (GObject *object, E_SPELL_ENTRY (object), g_value_get_boolean (value)); return; + + case PROP_SPELL_CHECKER: + e_spell_entry_set_spell_checker ( + E_SPELL_ENTRY (object), + g_value_get_object (value)); + return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -751,6 +722,13 @@ spell_entry_get_property (GObject *object, e_spell_entry_get_checking_enabled ( E_SPELL_ENTRY (object))); return; + + case PROP_SPELL_CHECKER: + g_value_set_object ( + value, + e_spell_entry_get_spell_checker ( + E_SPELL_ENTRY (object))); + return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -763,10 +741,14 @@ spell_entry_dispose (GObject *object) priv = E_SPELL_ENTRY_GET_PRIVATE (object); - g_slist_free_full (priv->checkers, (GDestroyNotify) g_object_unref); - priv->checkers = NULL; + if (priv->active_languages_handler_id > 0) { + g_signal_handler_disconnect ( + priv->spell_checker, + priv->active_languages_handler_id); + priv->active_languages_handler_id = 0; + } - g_clear_object (&priv->settings); + g_clear_object (&priv->spell_checker); if (priv->attr_list != NULL) { pango_attr_list_unref (priv->attr_list); @@ -795,9 +777,22 @@ spell_entry_finalize (GObject *object) static void spell_entry_constructed (GObject *object) { + ESpellEntry *spell_entry; + ESpellChecker *spell_checker; + + spell_entry = E_SPELL_ENTRY (object); + /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (e_spell_entry_parent_class)->constructed (object); + /* Install a default spell checker if there is not one already. */ + spell_checker = e_spell_entry_get_spell_checker (spell_entry); + if (spell_checker == NULL) { + spell_checker = e_spell_checker_new (); + e_spell_entry_set_spell_checker (spell_entry, spell_checker); + g_object_unref (spell_checker); + } + e_extensible_load_extensions (E_EXTENSIBLE (object)); } @@ -860,6 +855,17 @@ e_spell_entry_class_init (ESpellEntryClass *class) TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SPELL_CHECKER, + g_param_spec_object ( + "spell-checker", + "Spell Checker", + "The spell checker object", + E_TYPE_SPELL_CHECKER, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); } static void @@ -867,7 +873,6 @@ e_spell_entry_init (ESpellEntry *spell_entry) { spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry); spell_entry->priv->attr_list = pango_attr_list_new (); - spell_entry->priv->checkers = NULL; spell_entry->priv->checking_enabled = TRUE; g_signal_connect ( @@ -882,15 +887,6 @@ e_spell_entry_init (ESpellEntry *spell_entry) e_signal_connect_notify ( spell_entry, "notify::scroll-offset", G_CALLBACK (spell_entry_notify_scroll_offset), NULL); - - /* listen for languages changes */ - spell_entry->priv->settings = g_settings_new ("org.gnome.evolution.mail"); - g_signal_connect_swapped ( - spell_entry->priv->settings, "changed", - G_CALLBACK (spell_entry_settings_changed), spell_entry); - - /* load current settings */ - spell_entry_settings_changed (spell_entry, NULL); } GtkWidget * @@ -899,36 +895,6 @@ e_spell_entry_new (void) return g_object_new (E_TYPE_SPELL_ENTRY, NULL); } -/* 'languages' consists of 'const GtkhtmlSpellLanguage *' */ -void -e_spell_entry_set_languages (ESpellEntry *spell_entry, - GList *languages) -{ - GList *iter; - - g_return_if_fail (spell_entry != NULL); - - spell_entry->priv->custom_checkers = TRUE; - - if (spell_entry->priv->checkers) - g_slist_free_full (spell_entry->priv->checkers, g_object_unref); - spell_entry->priv->checkers = NULL; - - for (iter = languages; iter; iter = g_list_next (iter)) { - const GtkhtmlSpellLanguage *language = iter->data; - - if (language) - spell_entry->priv->checkers = g_slist_prepend ( - spell_entry->priv->checkers, - gtkhtml_spell_checker_new (language)); - } - - spell_entry->priv->checkers = g_slist_reverse (spell_entry->priv->checkers); - - if (gtk_widget_get_realized (GTK_WIDGET (spell_entry))) - spell_entry_recheck_all (spell_entry); -} - gboolean e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry) { @@ -951,3 +917,66 @@ e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry, g_object_notify (G_OBJECT (spell_entry), "checking-enabled"); } + +/** + * e_spell_entry_get_spell_checker: + * @spell_entry: an #ESpellEntry + * + * Returns the #ESpellChecker being used for spell checking. By default, + * #ESpellEntry creates its own #ESpellChecker, but this can be overridden + * through e_spell_entry_set_spell_checker(). + * + * Returns: an #ESpellChecker + **/ +ESpellChecker * +e_spell_entry_get_spell_checker (ESpellEntry *spell_entry) +{ + g_return_val_if_fail (E_IS_SPELL_ENTRY (spell_entry), NULL); + + return spell_entry->priv->spell_checker; +} + +/** + * e_spell_entry_set_spell_checker: + * @spell_entry: an #ESpellEntry + * @spell_checker: an #ESpellChecker + * + * Sets the #ESpellChecker to use for spell checking. By default, + * #ESpellEntry creates its own #ESpellChecker. This function can be + * useful for sharing an #ESpellChecker across multiple spell-checking + * widgets, so the active spell checking languages stay synchronized. + **/ +void +e_spell_entry_set_spell_checker (ESpellEntry *spell_entry, + ESpellChecker *spell_checker) +{ + gulong handler_id; + + g_return_if_fail (E_IS_SPELL_ENTRY (spell_entry)); + g_return_if_fail (E_IS_SPELL_CHECKER (spell_checker)); + + if (spell_checker == spell_entry->priv->spell_checker) + return; + + if (spell_entry->priv->spell_checker != NULL) { + g_signal_handler_disconnect ( + spell_entry->priv->spell_checker, + spell_entry->priv->active_languages_handler_id); + g_object_unref (spell_entry->priv->spell_checker); + } + + spell_entry->priv->spell_checker = g_object_ref (spell_checker); + + handler_id = g_signal_connect ( + spell_checker, "notify::active-languages", + G_CALLBACK (spell_entry_active_languages_cb), + spell_entry); + + spell_entry->priv->active_languages_handler_id = handler_id; + + g_object_notify (G_OBJECT (spell_entry), "spell-checker"); + + if (gtk_widget_get_realized (GTK_WIDGET (spell_entry))) + spell_entry_recheck_all (spell_entry); +} + diff --git a/e-util/e-spell-entry.h b/e-util/e-spell-entry.h index ed23cb0453..a07d68f177 100644 --- a/e-util/e-spell-entry.h +++ b/e-util/e-spell-entry.h @@ -24,6 +24,8 @@ #include <gtk/gtk.h> +#include <e-util/e-util.h> + /* Standard GObject macros */ #define E_TYPE_SPELL_ENTRY \ (e_spell_entry_get_type ()) @@ -60,13 +62,14 @@ struct _ESpellEntryClass { GType e_spell_entry_get_type (void) G_GNUC_CONST; GtkWidget * e_spell_entry_new (void); -void e_spell_entry_set_languages (ESpellEntry *spell_entry, - GList *languages); gboolean e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry); void e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry, gboolean enable_checking); +ESpellChecker * e_spell_entry_get_spell_checker (ESpellEntry *spell_entry); +void e_spell_entry_set_spell_checker (ESpellEntry *spell_entry, + ESpellChecker *spell_checker); G_END_DECLS diff --git a/e-util/e-util-enums.h b/e-util/e-util-enums.h index 9913e4d938..736a901e7a 100644 --- a/e-util/e-util-enums.h +++ b/e-util/e-util-enums.h @@ -124,6 +124,227 @@ typedef enum { E_DURATION_DAYS } EDurationType; +typedef enum { + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE = 0, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H1, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H2, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H3, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H4, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H5, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_H6, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PARAGRAPH, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_BLOCKQUOTE, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_PRE, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ADDRESS, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN, + E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA +} EHTMLEditorSelectionBlockFormat; + +/* The values match the actual size in <font size="n"> */ +typedef enum { + E_HTML_EDITOR_SELECTION_FONT_SIZE_TINY = 1, + E_HTML_EDITOR_SELECTION_FONT_SIZE_SMALL = 2, + E_HTML_EDITOR_SELECTION_FONT_SIZE_NORMAL = 3, + E_HTML_EDITOR_SELECTION_FONT_SIZE_BIG = 4, + E_HTML_EDITOR_SELECTION_FONT_SIZE_BIGGER = 5, + E_HTML_EDITOR_SELECTION_FONT_SIZE_LARGE = 6, + E_HTML_EDITOR_SELECTION_FONT_SIZE_VERY_LARGE = 7 +} EHTMLEditorSelectionFontSize; + +typedef enum { + E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT, + E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER, + E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT +} EHTMLEditorSelectionAlignment; + +typedef enum { + E_HTML_EDITOR_SELECTION_GRANULARITY_CHARACTER, + E_HTML_EDITOR_SELECTION_GRANULARITY_WORD +} EHTMLEditorSelectionGranularity; + +/** + * EHTMLEditorViewCommand: + * @E_HTML_EDITOR_VIEW_COMMAND_BACKGROUND_COLOR: + * Sets background color to given value. + * @E_HTML_EDITOR_VIEW_COMMAND_BOLD: + * Toggles bold formatting of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_COPY: + * Copies current selection to clipboard. + * @E_HTML_EDITOR_VIEW_COMMAND_CREATE_LINK: + * Converts current selection to a link that points to URL in value + * @E_HTML_EDITOR_VIEW_COMMAND_CUT: + * Cuts current selection to clipboard. + * @E_HTML_EDITOR_VIEW_COMMAND_DEFAULT_PARAGRAPH_SEPARATOR: + * (XXX Explain me!) + * @E_HTML_EDITOR_VIEW_COMMAND_DELETE: + * Deletes current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_FIND_STRING: + * Highlights given string. + * @E_HTML_EDITOR_VIEW_COMMAND_FONT_NAME: + * Sets font name to given value. + * @E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE: + * Sets font point size to given value (no units, just number) + * @E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE_DELTA: + * Changes font size by given delta value (no units, just number) + * @E_HTML_EDITOR_VIEW_COMMAND_FORE_COLOR: + * Sets font color to given value + * @E_HTML_EDITOR_VIEW_COMMAND_FORMAT_BLOCK: + * Sets block type of current paragraph to given format. Allowed formats + * are "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6", "P", "PRE" and + * "ADDRESS". + * @E_HTML_EDITOR_VIEW_COMMAND_FORWARD_DELETE: + * (XXX Explain me!) + * @E_HTML_EDITOR_VIEW_COMMAND_HILITE_COLOR: + * Sets color in which results of "FindString" command should be + * highlighted to given value. + * @E_HTML_EDITOR_VIEW_COMMAND_INDENT: + * Indents current paragraph by one level. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML: + * Inserts give HTML code into document. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_HORIZONTAL_RULE: + * Inserts a horizontal rule (<HR>) on current line. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_IMAGE: + * Inserts an image with given source file. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_LINE_BREAK: + * Breaks line at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT: + * Breaks citation at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_ORDERED_LIST: + * Creates an ordered list environment at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_PARAGRAPH: + * Inserts a new paragraph at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT: + * Inserts given text at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_INSERT_UNORDERED_LIST: + * Creates an undordered list environment at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_ITALIC: + * Toggles italic formatting of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_CENTER: + * Aligns current paragraph to center. + * @E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_FULL: + * Justifies current paragraph to block. + * @E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_NONE: + * Removes any justification or alignment of current paragraph. + * @E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_RIGHT: + * Aligns current paragraph to right. + * @E_HTML_EDITOR_VIEW_COMMAND_OUTDENT: + * Outdents current paragraph by one level. + * @E_HTML_EDITOR_VIEW_COMMAND_PASTE: + * Pastes clipboard content at current cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_PASTE_AND_MATCH_STYLE: + * Pastes clipboard content and matches its style to style at current + * cursor position. + * @E_HTML_EDITOR_VIEW_COMMAND_PASTE_AS_PLAIN_TEXT: + * Pastes clipboard content at current cursor position removing any HTML + * formatting. + * @E_HTML_EDITOR_VIEW_COMMAND_PRINT: + * Print current document. + * @E_HTML_EDITOR_VIEW_COMMAND_REDO: + * Redoes last action. + * @E_HTML_EDITOR_VIEW_COMMAND_REMOVE_FORMAT: + * Removes any formatting of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_SELECT_ALL: + * Extends selects to the entire document. + * @E_HTML_EDITOR_VIEW_COMMAND_STRIKETHROUGH: + * Toggles strikethrough formatting. + * @E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS: + * Toggles whether style should be defined in CSS "style" attribute of + * elements or whether to use deprecated <FONT> tags. Depends on + * whether given value is "true" or "false". + * @E_HTML_EDITOR_VIEW_COMMAND_SUBSCRIPT: + * Toggles subscript of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_SUPERSCRIPT: + * Toggles superscript of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_TRANSPOSE: + * (XXX Explain me!) + * @E_HTML_EDITOR_VIEW_COMMAND_UNDERLINE: + * Toggles underline formatting of current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_UNDO: + * Undoes last action. + * @E_HTML_EDITOR_VIEW_COMMAND_UNLINK: + * Removes active links (<A>) from current selection (if there's any). + * @E_HTML_EDITOR_VIEW_COMMAND_UNSELECT: + * Cancels current selection. + * @E_HTML_EDITOR_VIEW_COMMAND_USE_CSS: + * Whether to allow use of CSS or not depending on whether given value is + * "true" or "false". + * + * Specifies the DOM command to execute in e_editor_widget_exec_command(). + * Some commands require value to be passed in, which is always stated in the + * documentation. + */ +typedef enum { + E_HTML_EDITOR_VIEW_COMMAND_BACKGROUND_COLOR, + E_HTML_EDITOR_VIEW_COMMAND_BOLD, + E_HTML_EDITOR_VIEW_COMMAND_COPY, + E_HTML_EDITOR_VIEW_COMMAND_CREATE_LINK, + E_HTML_EDITOR_VIEW_COMMAND_CUT, + E_HTML_EDITOR_VIEW_COMMAND_DEFAULT_PARAGRAPH_SEPARATOR, + E_HTML_EDITOR_VIEW_COMMAND_DELETE, + E_HTML_EDITOR_VIEW_COMMAND_FIND_STRING, + E_HTML_EDITOR_VIEW_COMMAND_FONT_NAME, + E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE, + E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE_DELTA, + E_HTML_EDITOR_VIEW_COMMAND_FORE_COLOR, + E_HTML_EDITOR_VIEW_COMMAND_FORMAT_BLOCK, + E_HTML_EDITOR_VIEW_COMMAND_FORWARD_DELETE, + E_HTML_EDITOR_VIEW_COMMAND_HILITE_COLOR, + E_HTML_EDITOR_VIEW_COMMAND_INDENT, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_HORIZONTAL_RULE, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_IMAGE, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_LINE_BREAK, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_ORDERED_LIST, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_PARAGRAPH, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, + E_HTML_EDITOR_VIEW_COMMAND_INSERT_UNORDERED_LIST, + E_HTML_EDITOR_VIEW_COMMAND_ITALIC, + E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_CENTER, + E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_FULL, + E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_LEFT, + E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_NONE, + E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_RIGHT, + E_HTML_EDITOR_VIEW_COMMAND_OUTDENT, + E_HTML_EDITOR_VIEW_COMMAND_PASTE, + E_HTML_EDITOR_VIEW_COMMAND_PASTE_AND_MATCH_STYLE, + E_HTML_EDITOR_VIEW_COMMAND_PASTE_AS_PLAIN_TEXT, + E_HTML_EDITOR_VIEW_COMMAND_PRINT, + E_HTML_EDITOR_VIEW_COMMAND_REDO, + E_HTML_EDITOR_VIEW_COMMAND_REMOVE_FORMAT, + E_HTML_EDITOR_VIEW_COMMAND_SELECT_ALL, + E_HTML_EDITOR_VIEW_COMMAND_STRIKETHROUGH, + E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS, + E_HTML_EDITOR_VIEW_COMMAND_SUBSCRIPT, + E_HTML_EDITOR_VIEW_COMMAND_SUPERSCRIPT, + E_HTML_EDITOR_VIEW_COMMAND_TRANSPOSE, + E_HTML_EDITOR_VIEW_COMMAND_UNDERLINE, + E_HTML_EDITOR_VIEW_COMMAND_UNDO, + E_HTML_EDITOR_VIEW_COMMAND_UNLINK, + E_HTML_EDITOR_VIEW_COMMAND_UNSELECT, + E_HTML_EDITOR_VIEW_COMMAND_USE_CSS +} EHTMLEditorViewCommand; + +/** + * EImageLoadingPolicy: + * @E_IMAGE_LOADING_POLICY_NEVER: + * Never load images from a remote server. + * @E_IMAGE_LOADING_POLICY_SOMETIMES: + * Only load images from a remote server if the sender is a known contact. + * @E_IMAGE_LOADING_POLICY_ALWAYS: + * Always load images from a remote server. + * + * Policy for loading remote image URLs in email. Allowing images to be + * loaded from a remote server may have privacy implications. + **/ +typedef enum { + E_IMAGE_LOADING_POLICY_NEVER, + E_IMAGE_LOADING_POLICY_SOMETIMES, + E_IMAGE_LOADING_POLICY_ALWAYS +} EImageLoadingPolicy; + G_END_DECLS #endif /* E_UTIL_ENUMS_H */ diff --git a/e-util/e-util.h b/e-util/e-util.h index 784858e99f..e9b06fe27c 100644 --- a/e-util/e-util.h +++ b/e-util/e-util.h @@ -81,6 +81,8 @@ #include <e-util/e-client-cache.h> #include <e-util/e-client-combo-box.h> #include <e-util/e-client-selector.h> +#include <e-util/e-color-chooser-widget.h> +#include <e-util/e-color-combo.h> #include <e-util/e-config.h> #include <e-util/e-contact-store.h> #include <e-util/e-data-capture.h> @@ -89,6 +91,11 @@ #include <e-util/e-destination-store.h> #include <e-util/e-dialog-utils.h> #include <e-util/e-dialog-widgets.h> +#include <e-util/e-emoticon-action.h> +#include <e-util/e-emoticon-chooser-menu.h> +#include <e-util/e-emoticon-chooser.h> +#include <e-util/e-emoticon-tool-button.h> +#include <e-util/e-emoticon.h> #include <e-util/e-event.h> #include <e-util/e-file-request.h> #include <e-util/e-file-utils.h> @@ -103,9 +110,27 @@ #include <e-util/e-filter-part.h> #include <e-util/e-filter-rule.h> #include <e-util/e-focus-tracker.h> +#include <e-util/e-html-editor-actions.h> +#include <e-util/e-html-editor-cell-dialog.h> +#include <e-util/e-html-editor-dialog.h> +#include <e-util/e-html-editor-find-dialog.h> +#include <e-util/e-html-editor-hrule-dialog.h> +#include <e-util/e-html-editor-image-dialog.h> +#include <e-util/e-html-editor-link-dialog.h> +#include <e-util/e-html-editor-page-dialog.h> +#include <e-util/e-html-editor-paragraph-dialog.h> +#include <e-util/e-html-editor-replace-dialog.h> +#include <e-util/e-html-editor-selection.h> +#include <e-util/e-html-editor-spell-check-dialog.h> +#include <e-util/e-html-editor-table-dialog.h> +#include <e-util/e-html-editor-text-dialog.h> +#include <e-util/e-html-editor-utils.h> +#include <e-util/e-html-editor-view.h> +#include <e-util/e-html-editor.h> #include <e-util/e-html-utils.h> #include <e-util/e-icon-factory.h> #include <e-util/e-image-chooser.h> +#include <e-util/e-image-chooser-dialog.h> #include <e-util/e-import-assistant.h> #include <e-util/e-import.h> #include <e-util/e-interval-chooser.h> @@ -220,7 +245,6 @@ #include <e-util/e-url-entry.h> #include <e-util/e-util-enums.h> #include <e-util/e-util-enumtypes.h> -#include <e-util/e-web-view-gtkhtml.h> #include <e-util/e-web-view-preview.h> #include <e-util/e-web-view.h> #include <e-util/e-widget-undo.h> diff --git a/e-util/e-web-view-gtkhtml.c b/e-util/e-web-view-gtkhtml.c deleted file mode 100644 index 7963a1cbf9..0000000000 --- a/e-util/e-web-view-gtkhtml.c +++ /dev/null @@ -1,2352 +0,0 @@ -/* - * e-web-view-gtkhtml.c - * - * This program is free software; you can redistribute it and/or modify it - * under the terms 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, see <http://www.gnu.org/licenses/>. - * - */ - -#ifdef HAVE_CONFIG_H -#include <config.h> -#endif - -#include "e-web-view-gtkhtml.h" - -#include <string.h> -#include <glib/gi18n-lib.h> - -#include <camel/camel.h> -#include <libebackend/libebackend.h> - -#include "e-alert-dialog.h" -#include "e-alert-sink.h" -#include "e-misc-utils.h" -#include "e-plugin-ui.h" -#include "e-popup-action.h" -#include "e-selectable.h" - -#define E_WEB_VIEW_GTKHTML_GET_PRIVATE(obj) \ - (G_TYPE_INSTANCE_GET_PRIVATE \ - ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLPrivate)) - -typedef struct _EWebViewGtkHTMLRequest EWebViewGtkHTMLRequest; - -struct _EWebViewGtkHTMLPrivate { - GList *requests; - GtkUIManager *ui_manager; - gchar *selected_uri; - GdkPixbufAnimation *cursor_image; - - GtkAction *open_proxy; - GtkAction *print_proxy; - GtkAction *save_as_proxy; - - GtkTargetList *copy_target_list; - GtkTargetList *paste_target_list; - - /* Lockdown Options */ - guint disable_printing : 1; - guint disable_save_to_disk : 1; -}; - -struct _EWebViewGtkHTMLRequest { - GFile *file; - EWebViewGtkHTML *web_view; - GCancellable *cancellable; - GInputStream *input_stream; - GtkHTMLStream *output_stream; - gchar buffer[4096]; -}; - -enum { - PROP_0, - PROP_ANIMATE, - PROP_CARET_MODE, - PROP_COPY_TARGET_LIST, - PROP_DISABLE_PRINTING, - PROP_DISABLE_SAVE_TO_DISK, - PROP_EDITABLE, - PROP_INLINE_SPELLING, - PROP_MAGIC_LINKS, - PROP_MAGIC_SMILEYS, - PROP_OPEN_PROXY, - PROP_PASTE_TARGET_LIST, - PROP_PRINT_PROXY, - PROP_SAVE_AS_PROXY, - PROP_SELECTED_URI, - PROP_CURSOR_IMAGE -}; - -enum { - COPY_CLIPBOARD, - CUT_CLIPBOARD, - PASTE_CLIPBOARD, - POPUP_EVENT, - STATUS_MESSAGE, - STOP_LOADING, - UPDATE_ACTIONS, - PROCESS_MAILTO, - LAST_SIGNAL -}; - -static guint signals[LAST_SIGNAL]; - -static const gchar *ui = -"<ui>" -" <popup name='context'>" -" <menuitem action='copy-clipboard'/>" -" <separator/>" -" <placeholder name='custom-actions-1'>" -" <menuitem action='open'/>" -" <menuitem action='save-as'/>" -" <menuitem action='http-open'/>" -" <menuitem action='send-message'/>" -" <menuitem action='print'/>" -" </placeholder>" -" <placeholder name='custom-actions-2'>" -" <menuitem action='uri-copy'/>" -" <menuitem action='mailto-copy'/>" -" <menuitem action='image-copy'/>" -" </placeholder>" -" <placeholder name='custom-actions-3'/>" -" <separator/>" -" <menuitem action='select-all'/>" -" </popup>" -"</ui>"; - -/* Forward Declarations */ -static void e_web_view_gtkhtml_alert_sink_init (EAlertSinkInterface *iface); -static void e_web_view_gtkhtml_selectable_init (ESelectableInterface *iface); - -G_DEFINE_TYPE_WITH_CODE ( - EWebViewGtkHTML, - e_web_view_gtkhtml, - GTK_TYPE_HTML, - G_IMPLEMENT_INTERFACE ( - E_TYPE_EXTENSIBLE, NULL) - G_IMPLEMENT_INTERFACE ( - E_TYPE_ALERT_SINK, - e_web_view_gtkhtml_alert_sink_init) - G_IMPLEMENT_INTERFACE ( - E_TYPE_SELECTABLE, - e_web_view_gtkhtml_selectable_init)) - -static EWebViewGtkHTMLRequest * -web_view_gtkhtml_request_new (EWebViewGtkHTML *web_view, - const gchar *uri, - GtkHTMLStream *stream) -{ - EWebViewGtkHTMLRequest *request; - GList *list; - - request = g_slice_new (EWebViewGtkHTMLRequest); - - /* Try to detect file paths posing as URIs. */ - if (*uri == '/') - request->file = g_file_new_for_path (uri); - else - request->file = g_file_new_for_uri (uri); - - request->web_view = g_object_ref (web_view); - request->cancellable = g_cancellable_new (); - request->input_stream = NULL; - request->output_stream = stream; - - list = request->web_view->priv->requests; - list = g_list_prepend (list, request); - request->web_view->priv->requests = list; - - return request; -} - -static void -web_view_gtkhtml_request_free (EWebViewGtkHTMLRequest *request) -{ - GList *list; - - list = request->web_view->priv->requests; - list = g_list_remove (list, request); - request->web_view->priv->requests = list; - - g_object_unref (request->file); - g_object_unref (request->web_view); - g_object_unref (request->cancellable); - - if (request->input_stream != NULL) - g_object_unref (request->input_stream); - - g_slice_free (EWebViewGtkHTMLRequest, request); -} - -static void -web_view_gtkhtml_request_cancel (EWebViewGtkHTMLRequest *request) -{ - g_cancellable_cancel (request->cancellable); -} - -static gboolean -web_view_gtkhtml_request_check_for_error (EWebViewGtkHTMLRequest *request, - GError *error) -{ - GtkHTML *html; - GtkHTMLStream *stream; - - if (error == NULL) - return FALSE; - - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { - /* use this error, but do not close the stream */ - g_error_free (error); - return TRUE; - } - - /* XXX Should we log errors that are not cancellations? */ - - html = GTK_HTML (request->web_view); - stream = request->output_stream; - - gtk_html_end (html, stream, GTK_HTML_STREAM_ERROR); - web_view_gtkhtml_request_free (request); - g_error_free (error); - - return TRUE; -} - -static void -web_view_gtkhtml_request_stream_read_cb (GInputStream *input_stream, - GAsyncResult *result, - EWebViewGtkHTMLRequest *request) -{ - gssize bytes_read; - GError *error = NULL; - - bytes_read = g_input_stream_read_finish (input_stream, result, &error); - - if (web_view_gtkhtml_request_check_for_error (request, error)) - return; - - if (bytes_read == 0) { - gtk_html_end ( - GTK_HTML (request->web_view), - request->output_stream, GTK_HTML_STREAM_OK); - web_view_gtkhtml_request_free (request); - return; - } - - gtk_html_write ( - GTK_HTML (request->web_view), - request->output_stream, request->buffer, bytes_read); - - g_input_stream_read_async ( - request->input_stream, request->buffer, - sizeof (request->buffer), G_PRIORITY_DEFAULT, - request->cancellable, (GAsyncReadyCallback) - web_view_gtkhtml_request_stream_read_cb, request); -} - -static void -web_view_gtkhtml_request_read_cb (GFile *file, - GAsyncResult *result, - EWebViewGtkHTMLRequest *request) -{ - GFileInputStream *input_stream; - GError *error = NULL; - - /* Input stream might be NULL, so don't use cast macro. */ - input_stream = g_file_read_finish (file, result, &error); - request->input_stream = (GInputStream *) input_stream; - - if (web_view_gtkhtml_request_check_for_error (request, error)) - return; - - g_input_stream_read_async ( - request->input_stream, request->buffer, - sizeof (request->buffer), G_PRIORITY_DEFAULT, - request->cancellable, (GAsyncReadyCallback) - web_view_gtkhtml_request_stream_read_cb, request); -} - -static void -action_copy_clipboard_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - e_web_view_gtkhtml_copy_clipboard (web_view); -} - -static void -action_http_open_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - const gchar *uri; - gpointer parent; - - parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); - parent = gtk_widget_is_toplevel (parent) ? parent : NULL; - - uri = e_web_view_gtkhtml_get_selected_uri (web_view); - g_return_if_fail (uri != NULL); - - e_show_uri (parent, uri); -} - -static void -action_mailto_copy_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - CamelURL *curl; - CamelInternetAddress *inet_addr; - GtkClipboard *clipboard; - const gchar *uri; - gchar *text; - - uri = e_web_view_gtkhtml_get_selected_uri (web_view); - g_return_if_fail (uri != NULL); - - /* This should work because we checked it in update_actions(). */ - curl = camel_url_new (uri, NULL); - g_return_if_fail (curl != NULL); - - inet_addr = camel_internet_address_new (); - camel_address_decode (CAMEL_ADDRESS (inet_addr), curl->path); - text = camel_address_format (CAMEL_ADDRESS (inet_addr)); - if (text == NULL || *text == '\0') - text = g_strdup (uri + strlen ("mailto:")); - - g_object_unref (inet_addr); - camel_url_free (curl); - - clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); - gtk_clipboard_set_text (clipboard, text, -1); - gtk_clipboard_store (clipboard); - - clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); - gtk_clipboard_set_text (clipboard, text, -1); - gtk_clipboard_store (clipboard); - - g_free (text); -} - -static void -action_select_all_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - e_web_view_gtkhtml_select_all (web_view); -} - -static void -action_send_message_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - const gchar *uri; - gpointer parent; - gboolean handled; - - parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); - parent = gtk_widget_is_toplevel (parent) ? parent : NULL; - - uri = e_web_view_gtkhtml_get_selected_uri (web_view); - g_return_if_fail (uri != NULL); - - handled = FALSE; - g_signal_emit (web_view, signals[PROCESS_MAILTO], 0, uri, &handled); - - if (!handled) - e_show_uri (parent, uri); -} - -static void -action_uri_copy_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - GtkClipboard *clipboard; - const gchar *uri; - - clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); - uri = e_web_view_gtkhtml_get_selected_uri (web_view); - g_return_if_fail (uri != NULL); - - gtk_clipboard_set_text (clipboard, uri, -1); - gtk_clipboard_store (clipboard); -} - -static void -action_image_copy_cb (GtkAction *action, - EWebViewGtkHTML *web_view) -{ - GtkClipboard *clipboard; - GdkPixbufAnimation *animation; - GdkPixbuf *pixbuf; - - clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); - animation = e_web_view_gtkhtml_get_cursor_image (web_view); - g_return_if_fail (animation != NULL); - - pixbuf = gdk_pixbuf_animation_get_static_image (animation); - if (!pixbuf) - return; - - gtk_clipboard_set_image (clipboard, pixbuf); - gtk_clipboard_store (clipboard); -} - -static GtkActionEntry uri_entries[] = { - - { "uri-copy", - "edit-copy", - N_("_Copy Link Location"), - "<Control>c", - N_("Copy the link to the clipboard"), - G_CALLBACK (action_uri_copy_cb) } -}; - -static GtkActionEntry http_entries[] = { - - { "http-open", - "emblem-web", - N_("_Open Link in Browser"), - NULL, - N_("Open the link in a web browser"), - G_CALLBACK (action_http_open_cb) } -}; - -static GtkActionEntry mailto_entries[] = { - - { "mailto-copy", - "edit-copy", - N_("_Copy Email Address"), - "<Control>c", - N_("Copy the email address to the clipboard"), - G_CALLBACK (action_mailto_copy_cb) }, - - { "send-message", - "mail-message-new", - N_("_Send New Message To..."), - NULL, - N_("Send a mail message to this address"), - G_CALLBACK (action_send_message_cb) } -}; - -static GtkActionEntry image_entries[] = { - - { "image-copy", - "edit-copy", - N_("_Copy Image"), - "<Control>c", - N_("Copy the image to the clipboard"), - G_CALLBACK (action_image_copy_cb) } -}; - -static GtkActionEntry selection_entries[] = { - - { "copy-clipboard", - "edit-copy", - N_("_Copy"), - "<Control>c", - N_("Copy the selection"), - G_CALLBACK (action_copy_clipboard_cb) }, -}; - -static GtkActionEntry standard_entries[] = { - - { "select-all", - "edit-select-all", - N_("Select _All"), - NULL, - N_("Select all text and images"), - G_CALLBACK (action_select_all_cb) } -}; - -static gboolean -web_view_gtkhtml_button_press_event_cb (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkHTML *frame) -{ - gboolean event_handled = FALSE; - gchar *uri = NULL; - - if (event) { - GdkPixbufAnimation *anim; - - if (frame == NULL) - frame = GTK_HTML (web_view); - - anim = gtk_html_get_image_at (frame, event->x, event->y); - e_web_view_gtkhtml_set_cursor_image (web_view, anim); - if (anim != NULL) - g_object_unref (anim); - } - - if (event != NULL && event->button != 3) - return FALSE; - - /* Only extract a URI if no selection is active. Selected text - * implies the user is more likely to want to copy the selection - * to the clipboard than open a link within the selection. */ - if (!e_web_view_gtkhtml_is_selection_active (web_view)) - uri = e_web_view_gtkhtml_extract_uri (web_view, event, frame); - - if (uri != NULL && g_str_has_prefix (uri, "##")) { - g_free (uri); - return FALSE; - } - - g_signal_emit ( - web_view, signals[POPUP_EVENT], 0, - event, uri, &event_handled); - - g_free (uri); - - return event_handled; -} - -static void -web_view_gtkhtml_menu_item_select_cb (EWebViewGtkHTML *web_view, - GtkWidget *widget) -{ - GtkAction *action; - GtkActivatable *activatable; - const gchar *tooltip; - - activatable = GTK_ACTIVATABLE (widget); - action = gtk_activatable_get_related_action (activatable); - tooltip = gtk_action_get_tooltip (action); - - if (tooltip == NULL) - return; - - e_web_view_gtkhtml_status_message (web_view, tooltip); -} - -static void -web_view_gtkhtml_menu_item_deselect_cb (EWebViewGtkHTML *web_view) -{ - e_web_view_gtkhtml_status_message (web_view, NULL); -} - -static void -web_view_gtkhtml_connect_proxy_cb (EWebViewGtkHTML *web_view, - GtkAction *action, - GtkWidget *proxy) -{ - if (!GTK_IS_MENU_ITEM (proxy)) - return; - - g_signal_connect_swapped ( - proxy, "select", - G_CALLBACK (web_view_gtkhtml_menu_item_select_cb), web_view); - - g_signal_connect_swapped ( - proxy, "deselect", - G_CALLBACK (web_view_gtkhtml_menu_item_deselect_cb), web_view); -} - -static void -web_view_gtkhtml_set_property (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec) -{ - switch (property_id) { - case PROP_ANIMATE: - e_web_view_gtkhtml_set_animate ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_CARET_MODE: - e_web_view_gtkhtml_set_caret_mode ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_DISABLE_PRINTING: - e_web_view_gtkhtml_set_disable_printing ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_DISABLE_SAVE_TO_DISK: - e_web_view_gtkhtml_set_disable_save_to_disk ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_EDITABLE: - e_web_view_gtkhtml_set_editable ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_INLINE_SPELLING: - e_web_view_gtkhtml_set_inline_spelling ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_MAGIC_LINKS: - e_web_view_gtkhtml_set_magic_links ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_MAGIC_SMILEYS: - e_web_view_gtkhtml_set_magic_smileys ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_boolean (value)); - return; - - case PROP_OPEN_PROXY: - e_web_view_gtkhtml_set_open_proxy ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_object (value)); - return; - - case PROP_PRINT_PROXY: - e_web_view_gtkhtml_set_print_proxy ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_object (value)); - return; - - case PROP_SAVE_AS_PROXY: - e_web_view_gtkhtml_set_save_as_proxy ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_object (value)); - return; - - case PROP_SELECTED_URI: - e_web_view_gtkhtml_set_selected_uri ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_string (value)); - return; - case PROP_CURSOR_IMAGE: - e_web_view_gtkhtml_set_cursor_image ( - E_WEB_VIEW_GTKHTML (object), - g_value_get_object (value)); - return; - } - - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); -} - -static void -web_view_gtkhtml_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - switch (property_id) { - case PROP_ANIMATE: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_animate ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_CARET_MODE: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_caret_mode ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_COPY_TARGET_LIST: - g_value_set_boxed ( - value, e_web_view_gtkhtml_get_copy_target_list ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_DISABLE_PRINTING: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_disable_printing ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_DISABLE_SAVE_TO_DISK: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_disable_save_to_disk ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_EDITABLE: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_editable ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_INLINE_SPELLING: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_inline_spelling ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_MAGIC_LINKS: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_magic_links ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_MAGIC_SMILEYS: - g_value_set_boolean ( - value, e_web_view_gtkhtml_get_magic_smileys ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_OPEN_PROXY: - g_value_set_object ( - value, e_web_view_gtkhtml_get_open_proxy ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_PASTE_TARGET_LIST: - g_value_set_boxed ( - value, e_web_view_gtkhtml_get_paste_target_list ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_PRINT_PROXY: - g_value_set_object ( - value, e_web_view_gtkhtml_get_print_proxy ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_SAVE_AS_PROXY: - g_value_set_object ( - value, e_web_view_gtkhtml_get_save_as_proxy ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_SELECTED_URI: - g_value_set_string ( - value, e_web_view_gtkhtml_get_selected_uri ( - E_WEB_VIEW_GTKHTML (object))); - return; - - case PROP_CURSOR_IMAGE: - g_value_set_object ( - value, e_web_view_gtkhtml_get_cursor_image ( - E_WEB_VIEW_GTKHTML (object))); - return; - } - - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); -} - -static void -web_view_gtkhtml_dispose (GObject *object) -{ - EWebViewGtkHTMLPrivate *priv; - - priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (object); - - if (priv->ui_manager != NULL) { - g_object_unref (priv->ui_manager); - priv->ui_manager = NULL; - } - - if (priv->open_proxy != NULL) { - g_object_unref (priv->open_proxy); - priv->open_proxy = NULL; - } - - if (priv->print_proxy != NULL) { - g_object_unref (priv->print_proxy); - priv->print_proxy = NULL; - } - - if (priv->save_as_proxy != NULL) { - g_object_unref (priv->save_as_proxy); - priv->save_as_proxy = NULL; - } - - if (priv->copy_target_list != NULL) { - gtk_target_list_unref (priv->copy_target_list); - priv->copy_target_list = NULL; - } - - if (priv->paste_target_list != NULL) { - gtk_target_list_unref (priv->paste_target_list); - priv->paste_target_list = NULL; - } - - if (priv->cursor_image != NULL) { - g_object_unref (priv->cursor_image); - priv->cursor_image = NULL; - } - - /* Chain up to parent's dispose() method. */ - G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->dispose (object); -} - -static void -web_view_gtkhtml_finalize (GObject *object) -{ - EWebViewGtkHTMLPrivate *priv; - - priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (object); - - /* All URI requests should be complete or cancelled by now. */ - if (priv->requests != NULL) - g_warning ("Finalizing EWebViewGtkHTML with active URI requests"); - - g_free (priv->selected_uri); - - /* Chain up to parent's finalize() method. */ - G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->finalize (object); -} - -static void -web_view_gtkhtml_constructed (GObject *object) -{ -#ifndef G_OS_WIN32 - GSettings *settings; - - settings = g_settings_new ("org.gnome.desktop.lockdown"); - - g_settings_bind ( - settings, "disable-printing", - object, "disable-printing", - G_SETTINGS_BIND_GET); - - g_settings_bind ( - settings, "disable-save-to-disk", - object, "disable-save-to-disk", - G_SETTINGS_BIND_GET); - - g_object_unref (settings); -#endif - - /* Chain up to parent's constructed() method. */ - G_OBJECT_CLASS (e_web_view_gtkhtml_parent_class)->constructed (object); -} - -static gboolean -web_view_gtkhtml_button_press_event (GtkWidget *widget, - GdkEventButton *event) -{ - GtkWidgetClass *widget_class; - EWebViewGtkHTML *web_view; - - web_view = E_WEB_VIEW_GTKHTML (widget); - - if (web_view_gtkhtml_button_press_event_cb (web_view, event, NULL)) - return TRUE; - - /* Chain up to parent's button_press_event() method. */ - widget_class = GTK_WIDGET_CLASS (e_web_view_gtkhtml_parent_class); - return widget_class->button_press_event (widget, event); -} - -static gboolean -web_view_gtkhtml_scroll_event (GtkWidget *widget, - GdkEventScroll *event) -{ - if (event->state & GDK_CONTROL_MASK) { - GdkScrollDirection direction = event->direction; - - if (direction == GDK_SCROLL_SMOOTH) { - static gdouble total_delta_y = 0.0; - - total_delta_y += event->delta_y; - - if (total_delta_y >= 1.0) { - total_delta_y = 0.0; - direction = GDK_SCROLL_DOWN; - } else if (total_delta_y <= -1.0) { - total_delta_y = 0.0; - direction = GDK_SCROLL_UP; - } else { - return FALSE; - } - } - - switch (direction) { - case GDK_SCROLL_UP: - gtk_html_zoom_in (GTK_HTML (widget)); - return TRUE; - case GDK_SCROLL_DOWN: - gtk_html_zoom_out (GTK_HTML (widget)); - return TRUE; - default: - break; - } - } - - return FALSE; -} - -static void -web_view_gtkhtml_url_requested (GtkHTML *html, - const gchar *uri, - GtkHTMLStream *stream) -{ - EWebViewGtkHTMLRequest *request; - - request = web_view_gtkhtml_request_new (E_WEB_VIEW_GTKHTML (html), uri, stream); - - g_file_read_async ( - request->file, G_PRIORITY_DEFAULT, - request->cancellable, (GAsyncReadyCallback) - web_view_gtkhtml_request_read_cb, request); -} - -static void -web_view_gtkhtml_gtkhtml_link_clicked (GtkHTML *html, - const gchar *uri) -{ - EWebViewGtkHTMLClass *class; - EWebViewGtkHTML *web_view; - - web_view = E_WEB_VIEW_GTKHTML (html); - - class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); - g_return_if_fail (class->link_clicked != NULL); - - class->link_clicked (web_view, uri); -} - -static void -web_view_gtkhtml_on_url (GtkHTML *html, - const gchar *uri) -{ - EWebViewGtkHTMLClass *class; - EWebViewGtkHTML *web_view; - - web_view = E_WEB_VIEW_GTKHTML (html); - - class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); - g_return_if_fail (class->hovering_over_link != NULL); - - /* XXX WebKit would supply a title here. */ - class->hovering_over_link (web_view, NULL, uri); -} - -static void -web_view_gtkhtml_iframe_created (GtkHTML *html, - GtkHTML *iframe) -{ - g_signal_connect_swapped ( - iframe, "button-press-event", - G_CALLBACK (web_view_gtkhtml_button_press_event_cb), html); -} - -static gchar * -web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkHTML *html) -{ - gchar *uri; - - if (event != NULL) - uri = gtk_html_get_url_at (html, event->x, event->y); - else - uri = gtk_html_get_cursor_url (html); - - return uri; -} - -static void -web_view_gtkhtml_hovering_over_link (EWebViewGtkHTML *web_view, - const gchar *title, - const gchar *uri) -{ - CamelInternetAddress *address; - CamelURL *curl; - const gchar *format = NULL; - gchar *message = NULL; - gchar *who; - - if (uri == NULL || *uri == '\0') - goto exit; - - if (g_str_has_prefix (uri, "mailto:")) - format = _("Click to mail %s"); - else if (g_str_has_prefix (uri, "callto:") || - g_str_has_prefix (uri, "h323:") || - g_str_has_prefix (uri, "sip:") || - g_str_has_prefix (uri, "tel:")) - format = _("Click to call %s"); - else if (g_str_has_prefix (uri, "##")) - message = g_strdup (_("Click to hide/unhide addresses")); - else - message = g_strdup_printf (_("Click to open %s"), uri); - - if (format == NULL) - goto exit; - - /* XXX Use something other than Camel here. Surely - * there's other APIs around that can do this. */ - curl = camel_url_new (uri, NULL); - address = camel_internet_address_new (); - camel_address_decode (CAMEL_ADDRESS (address), curl->path); - who = camel_address_format (CAMEL_ADDRESS (address)); - g_object_unref (address); - camel_url_free (curl); - - if (who == NULL) - who = g_strdup (strchr (uri, ':') + 1); - - message = g_strdup_printf (format, who); - - g_free (who); - -exit: - e_web_view_gtkhtml_status_message (web_view, message); - - g_free (message); -} - -static void -web_view_gtkhtml_link_clicked (EWebViewGtkHTML *web_view, - const gchar *uri) -{ - gpointer parent; - - parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); - parent = gtk_widget_is_toplevel (parent) ? parent : NULL; - - e_show_uri (parent, uri); -} - -static void -web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, - const gchar *string) -{ - if (string != NULL && *string != '\0') - gtk_html_load_from_string (GTK_HTML (web_view), string, -1); - else - e_web_view_gtkhtml_clear (web_view); -} - -static void -web_view_gtkhtml_copy_clipboard (EWebViewGtkHTML *web_view) -{ - gtk_html_command (GTK_HTML (web_view), "copy"); -} - -static void -web_view_gtkhtml_cut_clipboard (EWebViewGtkHTML *web_view) -{ - if (e_web_view_gtkhtml_get_editable (web_view)) - gtk_html_command (GTK_HTML (web_view), "cut"); -} - -static void -web_view_gtkhtml_paste_clipboard (EWebViewGtkHTML *web_view) -{ - if (e_web_view_gtkhtml_get_editable (web_view)) - gtk_html_command (GTK_HTML (web_view), "paste"); -} - -static gboolean -web_view_gtkhtml_popup_event (EWebViewGtkHTML *web_view, - GdkEventButton *event, - const gchar *uri) -{ - e_web_view_gtkhtml_set_selected_uri (web_view, uri); - e_web_view_gtkhtml_show_popup_menu (web_view, event, NULL, NULL); - - return TRUE; -} - -static void -web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view) -{ - g_list_foreach ( - web_view->priv->requests, (GFunc) - web_view_gtkhtml_request_cancel, NULL); - - gtk_html_stop (GTK_HTML (web_view)); -} - -static void -web_view_gtkhtml_update_actions (EWebViewGtkHTML *web_view) -{ - GtkActionGroup *action_group; - gboolean have_selection; - gboolean scheme_is_http = FALSE; - gboolean scheme_is_mailto = FALSE; - gboolean uri_is_valid = FALSE; - gboolean has_cursor_image; - gboolean visible; - const gchar *group_name; - const gchar *uri; - - uri = e_web_view_gtkhtml_get_selected_uri (web_view); - have_selection = e_web_view_gtkhtml_is_selection_active (web_view); - has_cursor_image = e_web_view_gtkhtml_get_cursor_image (web_view) != NULL; - - /* Parse the URI early so we know if the actions will work. */ - if (uri != NULL) { - CamelURL *curl; - - curl = camel_url_new (uri, NULL); - uri_is_valid = (curl != NULL); - camel_url_free (curl); - - scheme_is_http = - (g_ascii_strncasecmp (uri, "http:", 5) == 0) || - (g_ascii_strncasecmp (uri, "https:", 6) == 0); - - scheme_is_mailto = - (g_ascii_strncasecmp (uri, "mailto:", 7) == 0); - } - - /* Allow copying the URI even if it's malformed. */ - group_name = "uri"; - visible = (uri != NULL) && !scheme_is_mailto; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "http"; - visible = uri_is_valid && scheme_is_http; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "mailto"; - visible = uri_is_valid && scheme_is_mailto; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "image"; - visible = has_cursor_image; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "selection"; - visible = have_selection; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "standard"; - visible = (uri == NULL); - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "lockdown-printing"; - visible = (uri == NULL) && !web_view->priv->disable_printing; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); - - group_name = "lockdown-save-to-disk"; - visible = (uri == NULL) && !web_view->priv->disable_save_to_disk; - action_group = e_web_view_gtkhtml_get_action_group (web_view, group_name); - gtk_action_group_set_visible (action_group, visible); -} - -static void -web_view_gtkhtml_submit_alert (EAlertSink *alert_sink, - EAlert *alert) -{ - GtkIconInfo *icon_info; - EWebViewGtkHTML *web_view; - GtkWidget *dialog; - GString *buffer; - const gchar *icon_name = NULL; - const gchar *filename; - gpointer parent; - gchar *icon_uri; - gint size = 0; - GError *error = NULL; - - web_view = E_WEB_VIEW_GTKHTML (alert_sink); - - parent = gtk_widget_get_toplevel (GTK_WIDGET (web_view)); - parent = gtk_widget_is_toplevel (parent) ? parent : NULL; - - /* We use equivalent named icons instead of stock IDs, - * since it's easier to get the filename of the icon. */ - switch (e_alert_get_message_type (alert)) { - case GTK_MESSAGE_INFO: - icon_name = "dialog-information"; - break; - - case GTK_MESSAGE_WARNING: - icon_name = "dialog-warning"; - break; - - case GTK_MESSAGE_ERROR: - icon_name = "dialog-error"; - break; - - default: - dialog = e_alert_dialog_new (parent, alert); - gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy (dialog); - return; - } - - gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &size, NULL); - - icon_info = gtk_icon_theme_lookup_icon ( - gtk_icon_theme_get_default (), - icon_name, size, GTK_ICON_LOOKUP_NO_SVG); - g_return_if_fail (icon_info != NULL); - - filename = gtk_icon_info_get_filename (icon_info); - icon_uri = g_filename_to_uri (filename, NULL, &error); - - if (error != NULL) { - g_warning ("%s", error->message); - g_clear_error (&error); - } - - buffer = g_string_sized_new (512); - - g_string_append ( - buffer, - "<html>" - "<head>" - "<meta http-equiv=\"content-type\"" - " content=\"text/html; charset=utf-8\">" - "</head>" - "<body>"); - - g_string_append ( - buffer, - "<table bgcolor='#000000' width='100%'" - " cellpadding='1' cellspacing='0'>" - "<tr>" - "<td>" - "<table bgcolor='#dddddd' width='100%' cellpadding='6'>" - "<tr>"); - - g_string_append_printf ( - buffer, - "<tr>" - "<td valign='top'>" - "<img src='%s'/>" - "</td>" - "<td align='left' width='100%%'>" - "<h3>%s</h3>" - "%s" - "</td>" - "</tr>", - icon_uri, - e_alert_get_primary_text (alert), - e_alert_get_secondary_text (alert)); - - g_string_append ( - buffer, - "</table>" - "</td>" - "</tr>" - "</table>" - "</body>" - "</html>"); - - e_web_view_gtkhtml_load_string (web_view, buffer->str); - - g_string_free (buffer, TRUE); - - gtk_icon_info_free (icon_info); - g_free (icon_uri); -} - -static void -web_view_gtkhtml_selectable_update_actions (ESelectable *selectable, - EFocusTracker *focus_tracker, - GdkAtom *clipboard_targets, - gint n_clipboard_targets) -{ - EWebViewGtkHTML *web_view; - GtkAction *action; - /*GtkTargetList *target_list;*/ - gboolean can_paste = FALSE; - gboolean editable; - gboolean have_selection; - gboolean sensitive; - const gchar *tooltip; - /*gint ii;*/ - - web_view = E_WEB_VIEW_GTKHTML (selectable); - editable = e_web_view_gtkhtml_get_editable (web_view); - have_selection = e_web_view_gtkhtml_is_selection_active (web_view); - - /* XXX GtkHtml implements its own clipboard instead of using - * GDK_SELECTION_CLIPBOARD, so we don't get notifications - * when the clipboard contents change. The logic below - * is what we would do if GtkHtml worked properly. - * Instead, we need to keep the Paste action sensitive so - * its accelerator overrides GtkHtml's key binding. */ -#if 0 - target_list = e_selectable_get_paste_target_list (selectable); - for (ii = 0; ii < n_clipboard_targets && !can_paste; ii++) - can_paste = gtk_target_list_find ( - target_list, clipboard_targets[ii], NULL); -#endif - can_paste = TRUE; - - action = e_focus_tracker_get_cut_clipboard_action (focus_tracker); - sensitive = editable && have_selection; - tooltip = _("Cut the selection"); - gtk_action_set_sensitive (action, sensitive); - gtk_action_set_tooltip (action, tooltip); - - action = e_focus_tracker_get_copy_clipboard_action (focus_tracker); - sensitive = have_selection; - tooltip = _("Copy the selection"); - gtk_action_set_sensitive (action, sensitive); - gtk_action_set_tooltip (action, tooltip); - - action = e_focus_tracker_get_paste_clipboard_action (focus_tracker); - sensitive = editable && can_paste; - tooltip = _("Paste the clipboard"); - gtk_action_set_sensitive (action, sensitive); - gtk_action_set_tooltip (action, tooltip); - - action = e_focus_tracker_get_select_all_action (focus_tracker); - sensitive = TRUE; - tooltip = _("Select all text and images"); - gtk_action_set_sensitive (action, sensitive); - gtk_action_set_tooltip (action, tooltip); -} - -static void -web_view_gtkhtml_selectable_cut_clipboard (ESelectable *selectable) -{ - e_web_view_gtkhtml_cut_clipboard (E_WEB_VIEW_GTKHTML (selectable)); -} - -static void -web_view_gtkhtml_selectable_copy_clipboard (ESelectable *selectable) -{ - e_web_view_gtkhtml_copy_clipboard (E_WEB_VIEW_GTKHTML (selectable)); -} - -static void -web_view_gtkhtml_selectable_paste_clipboard (ESelectable *selectable) -{ - e_web_view_gtkhtml_paste_clipboard (E_WEB_VIEW_GTKHTML (selectable)); -} - -static void -web_view_gtkhtml_selectable_select_all (ESelectable *selectable) -{ - e_web_view_gtkhtml_select_all (E_WEB_VIEW_GTKHTML (selectable)); -} - -static void -e_web_view_gtkhtml_class_init (EWebViewGtkHTMLClass *class) -{ - GObjectClass *object_class; - GtkWidgetClass *widget_class; - GtkHTMLClass *html_class; - - g_type_class_add_private (class, sizeof (EWebViewGtkHTMLPrivate)); - - object_class = G_OBJECT_CLASS (class); - object_class->set_property = web_view_gtkhtml_set_property; - object_class->get_property = web_view_gtkhtml_get_property; - object_class->dispose = web_view_gtkhtml_dispose; - object_class->finalize = web_view_gtkhtml_finalize; - object_class->constructed = web_view_gtkhtml_constructed; - - widget_class = GTK_WIDGET_CLASS (class); - widget_class->button_press_event = web_view_gtkhtml_button_press_event; - widget_class->scroll_event = web_view_gtkhtml_scroll_event; - - html_class = GTK_HTML_CLASS (class); - html_class->url_requested = web_view_gtkhtml_url_requested; - html_class->link_clicked = web_view_gtkhtml_gtkhtml_link_clicked; - html_class->on_url = web_view_gtkhtml_on_url; - html_class->iframe_created = web_view_gtkhtml_iframe_created; - - class->extract_uri = web_view_gtkhtml_extract_uri; - class->hovering_over_link = web_view_gtkhtml_hovering_over_link; - class->link_clicked = web_view_gtkhtml_link_clicked; - class->load_string = web_view_gtkhtml_load_string; - class->copy_clipboard = web_view_gtkhtml_copy_clipboard; - class->cut_clipboard = web_view_gtkhtml_cut_clipboard; - class->paste_clipboard = web_view_gtkhtml_paste_clipboard; - class->popup_event = web_view_gtkhtml_popup_event; - class->stop_loading = web_view_gtkhtml_stop_loading; - class->update_actions = web_view_gtkhtml_update_actions; - - g_object_class_install_property ( - object_class, - PROP_ANIMATE, - g_param_spec_boolean ( - "animate", - "Animate Images", - NULL, - FALSE, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_CARET_MODE, - g_param_spec_boolean ( - "caret-mode", - "Caret Mode", - NULL, - FALSE, - G_PARAM_READWRITE)); - - /* Inherited from ESelectableInterface */ - g_object_class_override_property ( - object_class, - PROP_COPY_TARGET_LIST, - "copy-target-list"); - - g_object_class_install_property ( - object_class, - PROP_DISABLE_PRINTING, - g_param_spec_boolean ( - "disable-printing", - "Disable Printing", - NULL, - FALSE, - G_PARAM_READWRITE | - G_PARAM_CONSTRUCT)); - - g_object_class_install_property ( - object_class, - PROP_DISABLE_SAVE_TO_DISK, - g_param_spec_boolean ( - "disable-save-to-disk", - "Disable Save-to-Disk", - NULL, - FALSE, - G_PARAM_READWRITE | - G_PARAM_CONSTRUCT)); - - g_object_class_install_property ( - object_class, - PROP_EDITABLE, - g_param_spec_boolean ( - "editable", - "Editable", - NULL, - FALSE, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_INLINE_SPELLING, - g_param_spec_boolean ( - "inline-spelling", - "Inline Spelling", - NULL, - FALSE, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_MAGIC_LINKS, - g_param_spec_boolean ( - "magic-links", - "Magic Links", - NULL, - FALSE, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_MAGIC_SMILEYS, - g_param_spec_boolean ( - "magic-smileys", - "Magic Smileys", - NULL, - FALSE, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_OPEN_PROXY, - g_param_spec_object ( - "open-proxy", - "Open Proxy", - NULL, - GTK_TYPE_ACTION, - G_PARAM_READWRITE)); - - /* Inherited from ESelectableInterface */ - g_object_class_override_property ( - object_class, - PROP_PASTE_TARGET_LIST, - "paste-target-list"); - - g_object_class_install_property ( - object_class, - PROP_PRINT_PROXY, - g_param_spec_object ( - "print-proxy", - "Print Proxy", - NULL, - GTK_TYPE_ACTION, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_SAVE_AS_PROXY, - g_param_spec_object ( - "save-as-proxy", - "Save As Proxy", - NULL, - GTK_TYPE_ACTION, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_SELECTED_URI, - g_param_spec_string ( - "selected-uri", - "Selected URI", - NULL, - NULL, - G_PARAM_READWRITE)); - - g_object_class_install_property ( - object_class, - PROP_CURSOR_IMAGE, - g_param_spec_object ( - "cursor-image", - "Image animation at the mouse cursor", - NULL, - GDK_TYPE_PIXBUF_ANIMATION, - G_PARAM_READWRITE)); - - signals[COPY_CLIPBOARD] = g_signal_new ( - "copy-clipboard", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, copy_clipboard), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - signals[CUT_CLIPBOARD] = g_signal_new ( - "cut-clipboard", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, cut_clipboard), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - signals[PASTE_CLIPBOARD] = g_signal_new ( - "paste-clipboard", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, paste_clipboard), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - signals[POPUP_EVENT] = g_signal_new ( - "popup-event", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, popup_event), - g_signal_accumulator_true_handled, NULL, - e_marshal_BOOLEAN__BOXED_STRING, - G_TYPE_BOOLEAN, 2, - GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE, - G_TYPE_STRING); - - signals[STATUS_MESSAGE] = g_signal_new ( - "status-message", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, status_message), - NULL, NULL, - g_cclosure_marshal_VOID__STRING, - G_TYPE_NONE, 1, - G_TYPE_STRING); - - signals[STOP_LOADING] = g_signal_new ( - "stop-loading", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, stop_loading), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - signals[UPDATE_ACTIONS] = g_signal_new ( - "update-actions", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, update_actions), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - /* return TRUE when a signal handler processed the mailto URI */ - signals[PROCESS_MAILTO] = g_signal_new ( - "process-mailto", - G_TYPE_FROM_CLASS (class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (EWebViewGtkHTMLClass, process_mailto), - NULL, NULL, - e_marshal_BOOLEAN__STRING, - G_TYPE_BOOLEAN, 1, G_TYPE_STRING); -} - -static void -e_web_view_gtkhtml_alert_sink_init (EAlertSinkInterface *iface) -{ - iface->submit_alert = web_view_gtkhtml_submit_alert; -} - -static void -e_web_view_gtkhtml_selectable_init (ESelectableInterface *iface) -{ - iface->update_actions = web_view_gtkhtml_selectable_update_actions; - iface->cut_clipboard = web_view_gtkhtml_selectable_cut_clipboard; - iface->copy_clipboard = web_view_gtkhtml_selectable_copy_clipboard; - iface->paste_clipboard = web_view_gtkhtml_selectable_paste_clipboard; - iface->select_all = web_view_gtkhtml_selectable_select_all; -} - -static void -e_web_view_gtkhtml_init (EWebViewGtkHTML *web_view) -{ - GtkUIManager *ui_manager; - GtkActionGroup *action_group; - GtkTargetList *target_list; - EPopupAction *popup_action; - const gchar *domain = GETTEXT_PACKAGE; - const gchar *id; - GError *error = NULL; - - web_view->priv = E_WEB_VIEW_GTKHTML_GET_PRIVATE (web_view); - - ui_manager = gtk_ui_manager_new (); - web_view->priv->ui_manager = ui_manager; - - g_signal_connect_swapped ( - ui_manager, "connect-proxy", - G_CALLBACK (web_view_gtkhtml_connect_proxy_cb), web_view); - - target_list = gtk_target_list_new (NULL, 0); - web_view->priv->copy_target_list = target_list; - - target_list = gtk_target_list_new (NULL, 0); - web_view->priv->paste_target_list = target_list; - - action_group = gtk_action_group_new ("uri"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, uri_entries, - G_N_ELEMENTS (uri_entries), web_view); - - action_group = gtk_action_group_new ("http"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, http_entries, - G_N_ELEMENTS (http_entries), web_view); - - action_group = gtk_action_group_new ("mailto"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, mailto_entries, - G_N_ELEMENTS (mailto_entries), web_view); - - action_group = gtk_action_group_new ("image"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, image_entries, - G_N_ELEMENTS (image_entries), web_view); - - action_group = gtk_action_group_new ("selection"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, selection_entries, - G_N_ELEMENTS (selection_entries), web_view); - - action_group = gtk_action_group_new ("standard"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - gtk_action_group_add_actions ( - action_group, standard_entries, - G_N_ELEMENTS (standard_entries), web_view); - - popup_action = e_popup_action_new ("open"); - gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); - g_object_unref (popup_action); - - g_object_bind_property ( - web_view, "open-proxy", - popup_action, "related-action", - G_BINDING_BIDIRECTIONAL | - G_BINDING_SYNC_CREATE); - - /* Support lockdown. */ - - action_group = gtk_action_group_new ("lockdown-printing"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - popup_action = e_popup_action_new ("print"); - gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); - g_object_unref (popup_action); - - g_object_bind_property ( - web_view, "print-proxy", - popup_action, "related-action", - G_BINDING_BIDIRECTIONAL | - G_BINDING_SYNC_CREATE); - - action_group = gtk_action_group_new ("lockdown-save-to-disk"); - gtk_action_group_set_translation_domain (action_group, domain); - gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); - g_object_unref (action_group); - - popup_action = e_popup_action_new ("save-as"); - gtk_action_group_add_action (action_group, GTK_ACTION (popup_action)); - g_object_unref (popup_action); - - g_object_bind_property ( - web_view, "save-as-proxy", - popup_action, "related-action", - G_BINDING_BIDIRECTIONAL | - G_BINDING_SYNC_CREATE); - - /* Because we are loading from a hard-coded string, there is - * no chance of I/O errors. Failure here implies a malformed - * UI definition. Full stop. */ - gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error); - if (error != NULL) - g_error ("%s", error->message); - - id = "org.gnome.evolution.webview"; - e_plugin_ui_register_manager (ui_manager, id, web_view); - e_plugin_ui_enable_manager (ui_manager, id); - - e_extensible_load_extensions (E_EXTENSIBLE (web_view)); -} - -GtkWidget * -e_web_view_gtkhtml_new (void) -{ - return g_object_new (E_TYPE_WEB_VIEW_GTKHTML, NULL); -} - -void -e_web_view_gtkhtml_clear (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_load_empty (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, - const gchar *string) -{ - EWebViewGtkHTMLClass *class; - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); - g_return_if_fail (class->load_string != NULL); - - class->load_string (web_view, string); -} - -gboolean -e_web_view_gtkhtml_get_animate (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_animate(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_animate (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_animate (EWebViewGtkHTML *web_view, - gboolean animate) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_animate() - * so we can get a "notify::animate" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_animate (GTK_HTML (web_view)) == animate) - return; - - gtk_html_set_animate (GTK_HTML (web_view), animate); - - g_object_notify (G_OBJECT (web_view), "animate"); -} - -gboolean -e_web_view_gtkhtml_get_caret_mode (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_caret_mode(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_caret_mode (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_caret_mode (EWebViewGtkHTML *web_view, - gboolean caret_mode) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_caret_mode() - * so we can get a "notify::caret-mode" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_caret_mode (GTK_HTML (web_view)) == caret_mode) - return; - - gtk_html_set_caret_mode (GTK_HTML (web_view), caret_mode); - - g_object_notify (G_OBJECT (web_view), "caret-mode"); -} - -GtkTargetList * -e_web_view_gtkhtml_get_copy_target_list (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->copy_target_list; -} - -gboolean -e_web_view_gtkhtml_get_disable_printing (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return web_view->priv->disable_printing; -} - -void -e_web_view_gtkhtml_set_disable_printing (EWebViewGtkHTML *web_view, - gboolean disable_printing) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->disable_printing == disable_printing) - return; - - web_view->priv->disable_printing = disable_printing; - - g_object_notify (G_OBJECT (web_view), "disable-printing"); -} - -gboolean -e_web_view_gtkhtml_get_disable_save_to_disk (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return web_view->priv->disable_save_to_disk; -} - -void -e_web_view_gtkhtml_set_disable_save_to_disk (EWebViewGtkHTML *web_view, - gboolean disable_save_to_disk) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->disable_save_to_disk == disable_save_to_disk) - return; - - web_view->priv->disable_save_to_disk = disable_save_to_disk; - - g_object_notify (G_OBJECT (web_view), "disable-save-to-disk"); -} - -gboolean -e_web_view_gtkhtml_get_editable (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_editable(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_editable (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_editable (EWebViewGtkHTML *web_view, - gboolean editable) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_editable() - * so we can get a "notify::editable" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_editable (GTK_HTML (web_view)) == editable) - return; - - gtk_html_set_editable (GTK_HTML (web_view), editable); - - g_object_notify (G_OBJECT (web_view), "editable"); -} - -gboolean -e_web_view_gtkhtml_get_inline_spelling (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_inline_spelling(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_inline_spelling (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_inline_spelling (EWebViewGtkHTML *web_view, - gboolean inline_spelling) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_inline_spelling() - * so we get a "notify::inline-spelling" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_inline_spelling (GTK_HTML (web_view)) == inline_spelling) - return; - - gtk_html_set_inline_spelling (GTK_HTML (web_view), inline_spelling); - - g_object_notify (G_OBJECT (web_view), "inline-spelling"); -} - -gboolean -e_web_view_gtkhtml_get_magic_links (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_magic_links(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_magic_links (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_magic_links (EWebViewGtkHTML *web_view, - gboolean magic_links) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_magic_links() - * so we can get a "notify::magic-links" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_magic_links (GTK_HTML (web_view)) == magic_links) - return; - - gtk_html_set_magic_links (GTK_HTML (web_view), magic_links); - - g_object_notify (G_OBJECT (web_view), "magic-links"); -} - -gboolean -e_web_view_gtkhtml_get_magic_smileys (EWebViewGtkHTML *web_view) -{ - /* XXX This is just here to maintain symmetry - * with e_web_view_set_magic_smileys(). */ - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_get_magic_smileys (GTK_HTML (web_view)); -} - -void -e_web_view_gtkhtml_set_magic_smileys (EWebViewGtkHTML *web_view, - gboolean magic_smileys) -{ - /* XXX GtkHTML does not utilize GObject properties as well - * as it could. This just wraps gtk_html_set_magic_smileys() - * so we can get a "notify::magic-smileys" signal. */ - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (gtk_html_get_magic_smileys (GTK_HTML (web_view)) == magic_smileys) - return; - - gtk_html_set_magic_smileys (GTK_HTML (web_view), magic_smileys); - - g_object_notify (G_OBJECT (web_view), "magic-smileys"); -} - -const gchar * -e_web_view_gtkhtml_get_selected_uri (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->selected_uri; -} - -void -e_web_view_gtkhtml_set_selected_uri (EWebViewGtkHTML *web_view, - const gchar *selected_uri) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (g_strcmp0 (web_view->priv->selected_uri, selected_uri) == 0) - return; - - g_free (web_view->priv->selected_uri); - web_view->priv->selected_uri = g_strdup (selected_uri); - - g_object_notify (G_OBJECT (web_view), "selected-uri"); -} - -GdkPixbufAnimation * -e_web_view_gtkhtml_get_cursor_image (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->cursor_image; -} - -void -e_web_view_gtkhtml_set_cursor_image (EWebViewGtkHTML *web_view, - GdkPixbufAnimation *image) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->cursor_image == image) - return; - - if (image != NULL) - g_object_ref (image); - - if (web_view->priv->cursor_image != NULL) - g_object_unref (web_view->priv->cursor_image); - - web_view->priv->cursor_image = image; - - g_object_notify (G_OBJECT (web_view), "cursor-image"); -} - -GtkAction * -e_web_view_gtkhtml_get_open_proxy (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->open_proxy; -} - -void -e_web_view_gtkhtml_set_open_proxy (EWebViewGtkHTML *web_view, - GtkAction *open_proxy) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->open_proxy == open_proxy) - return; - - if (open_proxy != NULL) { - g_return_if_fail (GTK_IS_ACTION (open_proxy)); - g_object_ref (open_proxy); - } - - if (web_view->priv->open_proxy != NULL) - g_object_unref (web_view->priv->open_proxy); - - web_view->priv->open_proxy = open_proxy; - - g_object_notify (G_OBJECT (web_view), "open-proxy"); -} - -GtkTargetList * -e_web_view_gtkhtml_get_paste_target_list (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->paste_target_list; -} - -GtkAction * -e_web_view_gtkhtml_get_print_proxy (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->print_proxy; -} - -void -e_web_view_gtkhtml_set_print_proxy (EWebViewGtkHTML *web_view, - GtkAction *print_proxy) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->print_proxy == print_proxy) - return; - - if (print_proxy != NULL) { - g_return_if_fail (GTK_IS_ACTION (print_proxy)); - g_object_ref (print_proxy); - } - - if (web_view->priv->print_proxy != NULL) - g_object_unref (web_view->priv->print_proxy); - - web_view->priv->print_proxy = print_proxy; - - g_object_notify (G_OBJECT (web_view), "print-proxy"); -} - -GtkAction * -e_web_view_gtkhtml_get_save_as_proxy (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->save_as_proxy; -} - -void -e_web_view_gtkhtml_set_save_as_proxy (EWebViewGtkHTML *web_view, - GtkAction *save_as_proxy) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - if (web_view->priv->save_as_proxy == save_as_proxy) - return; - - if (save_as_proxy != NULL) { - g_return_if_fail (GTK_IS_ACTION (save_as_proxy)); - g_object_ref (save_as_proxy); - } - - if (web_view->priv->save_as_proxy != NULL) - g_object_unref (web_view->priv->save_as_proxy); - - web_view->priv->save_as_proxy = save_as_proxy; - - g_object_notify (G_OBJECT (web_view), "save-as-proxy"); -} - -GtkAction * -e_web_view_gtkhtml_get_action (EWebViewGtkHTML *web_view, - const gchar *action_name) -{ - GtkUIManager *ui_manager; - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - g_return_val_if_fail (action_name != NULL, NULL); - - ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); - - return e_lookup_action (ui_manager, action_name); -} - -GtkActionGroup * -e_web_view_gtkhtml_get_action_group (EWebViewGtkHTML *web_view, - const gchar *group_name) -{ - GtkUIManager *ui_manager; - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - g_return_val_if_fail (group_name != NULL, NULL); - - ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); - - return e_lookup_action_group (ui_manager, group_name); -} - -gchar * -e_web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkHTML *frame) -{ - EWebViewGtkHTMLClass *class; - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - if (frame == NULL) - frame = GTK_HTML (web_view); - - class = E_WEB_VIEW_GTKHTML_GET_CLASS (web_view); - g_return_val_if_fail (class->extract_uri != NULL, NULL); - - return class->extract_uri (web_view, event, frame); -} - -void -e_web_view_gtkhtml_copy_clipboard (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[COPY_CLIPBOARD], 0); -} - -void -e_web_view_gtkhtml_cut_clipboard (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[CUT_CLIPBOARD], 0); -} - -gboolean -e_web_view_gtkhtml_is_selection_active (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_command (GTK_HTML (web_view), "is-selection-active"); -} - -void -e_web_view_gtkhtml_paste_clipboard (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[PASTE_CLIPBOARD], 0); -} - -gboolean -e_web_view_gtkhtml_scroll_forward (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_command (GTK_HTML (web_view), "scroll-forward"); -} - -gboolean -e_web_view_gtkhtml_scroll_backward (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), FALSE); - - return gtk_html_command (GTK_HTML (web_view), "scroll-backward"); -} - -void -e_web_view_gtkhtml_select_all (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_command (GTK_HTML (web_view), "select-all"); -} - -void -e_web_view_gtkhtml_unselect_all (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_command (GTK_HTML (web_view), "unselect-all"); -} - -void -e_web_view_gtkhtml_zoom_100 (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_command (GTK_HTML (web_view), "zoom-reset"); -} - -void -e_web_view_gtkhtml_zoom_in (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_command (GTK_HTML (web_view), "zoom-in"); -} - -void -e_web_view_gtkhtml_zoom_out (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - gtk_html_command (GTK_HTML (web_view), "zoom-out"); -} - -GtkUIManager * -e_web_view_gtkhtml_get_ui_manager (EWebViewGtkHTML *web_view) -{ - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - return web_view->priv->ui_manager; -} - -GtkWidget * -e_web_view_gtkhtml_get_popup_menu (EWebViewGtkHTML *web_view) -{ - GtkUIManager *ui_manager; - GtkWidget *menu; - - g_return_val_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view), NULL); - - ui_manager = e_web_view_gtkhtml_get_ui_manager (web_view); - menu = gtk_ui_manager_get_widget (ui_manager, "/context"); - g_return_val_if_fail (GTK_IS_MENU (menu), NULL); - - return menu; -} - -void -e_web_view_gtkhtml_show_popup_menu (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkMenuPositionFunc func, - gpointer user_data) -{ - GtkWidget *menu; - - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - e_web_view_gtkhtml_update_actions (web_view); - - menu = e_web_view_gtkhtml_get_popup_menu (web_view); - - if (event != NULL) - gtk_menu_popup ( - GTK_MENU (menu), NULL, NULL, func, - user_data, event->button, event->time); - else - gtk_menu_popup ( - GTK_MENU (menu), NULL, NULL, func, - user_data, 0, gtk_get_current_event_time ()); -} - -void -e_web_view_gtkhtml_status_message (EWebViewGtkHTML *web_view, - const gchar *status_message) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[STATUS_MESSAGE], 0, status_message); -} - -void -e_web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[STOP_LOADING], 0); -} - -void -e_web_view_gtkhtml_update_actions (EWebViewGtkHTML *web_view) -{ - g_return_if_fail (E_IS_WEB_VIEW_GTKHTML (web_view)); - - g_signal_emit (web_view, signals[UPDATE_ACTIONS], 0); -} diff --git a/e-util/e-web-view-gtkhtml.h b/e-util/e-web-view-gtkhtml.h deleted file mode 100644 index 4ad08e9cf6..0000000000 --- a/e-util/e-web-view-gtkhtml.h +++ /dev/null @@ -1,209 +0,0 @@ -/* - * e-web-view-gtkhtml.h - * - * This program is free software; you can redistribute it and/or modify it - * under the terms 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, see <http://www.gnu.org/licenses/>. - * - */ - -/* This is intended to serve as a common base class for all HTML viewing - * needs in Evolution. Currently based on GtkHTML, the idea is to wrap - * the GtkHTML API enough that we no longer have to make direct calls to - * it. This should help smooth the transition to WebKit/GTK+. - * - * This class handles basic tasks like mouse hovers over links, clicked - * links, and servicing URI requests asynchronously via GIO. */ - -#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) -#error "Only <e-util/e-util.h> should be included directly." -#endif - -#ifndef E_WEB_VIEW_GTKHTML_H -#define E_WEB_VIEW_GTKHTML_H - -#include <gtkhtml/gtkhtml.h> - -/* Standard GObject macros */ -#define E_TYPE_WEB_VIEW_GTKHTML \ - (e_web_view_gtkhtml_get_type ()) -#define E_WEB_VIEW_GTKHTML(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST \ - ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTML)) -#define E_WEB_VIEW_GTKHTML_CLASS(cls) \ - (G_TYPE_CHECK_CLASS_CAST \ - ((cls), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLClass)) -#define E_IS_WEB_VIEW_GTKHTML(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE \ - ((obj), E_TYPE_WEB_VIEW_GTKHTML)) -#define E_IS_WEB_VIEW_GTKHTML_CLASS(cls) \ - (G_TYPE_CHECK_CLASS_TYPE \ - ((cls), E_TYPE_WEB_VIEW_GTKHTML)) -#define E_WEB_VIEW_GTKHTML_GET_CLASS(obj) \ - (G_TYPE_INSTANCE_GET_CLASS \ - ((obj), E_TYPE_WEB_VIEW_GTKHTML, EWebViewGtkHTMLClass)) - -G_BEGIN_DECLS - -typedef struct _EWebViewGtkHTML EWebViewGtkHTML; -typedef struct _EWebViewGtkHTMLClass EWebViewGtkHTMLClass; -typedef struct _EWebViewGtkHTMLPrivate EWebViewGtkHTMLPrivate; - -struct _EWebViewGtkHTML { - GtkHTML parent; - EWebViewGtkHTMLPrivate *priv; -}; - -struct _EWebViewGtkHTMLClass { - GtkHTMLClass parent_class; - - /* Methods */ - gchar * (*extract_uri) (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkHTML *frame); - void (*hovering_over_link) (EWebViewGtkHTML *web_view, - const gchar *title, - const gchar *uri); - void (*link_clicked) (EWebViewGtkHTML *web_view, - const gchar *uri); - void (*load_string) (EWebViewGtkHTML *web_view, - const gchar *load_string); - - /* Signals */ - void (*copy_clipboard) (EWebViewGtkHTML *web_view); - void (*cut_clipboard) (EWebViewGtkHTML *web_view); - void (*paste_clipboard) (EWebViewGtkHTML *web_view); - gboolean (*popup_event) (EWebViewGtkHTML *web_view, - GdkEventButton *event, - const gchar *uri); - void (*status_message) (EWebViewGtkHTML *web_view, - const gchar *status_message); - void (*stop_loading) (EWebViewGtkHTML *web_view); - void (*update_actions) (EWebViewGtkHTML *web_view); - gboolean (*process_mailto) (EWebViewGtkHTML *web_view, - const gchar *mailto_uri); -}; - -GType e_web_view_gtkhtml_get_type (void) G_GNUC_CONST; -GtkWidget * e_web_view_gtkhtml_new (void); -void e_web_view_gtkhtml_clear (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_load_string (EWebViewGtkHTML *web_view, - const gchar *string); -gboolean e_web_view_gtkhtml_get_animate (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_animate (EWebViewGtkHTML *web_view, - gboolean animate); -gboolean e_web_view_gtkhtml_get_caret_mode - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_caret_mode - (EWebViewGtkHTML *web_view, - gboolean caret_mode); -GtkTargetList * e_web_view_gtkhtml_get_copy_target_list - (EWebViewGtkHTML *web_view); -gboolean e_web_view_gtkhtml_get_disable_printing - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_disable_printing - (EWebViewGtkHTML *web_view, - gboolean disable_printing); -gboolean e_web_view_gtkhtml_get_disable_save_to_disk - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_disable_save_to_disk - (EWebViewGtkHTML *web_view, - gboolean disable_save_to_disk); -gboolean e_web_view_gtkhtml_get_editable (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_editable (EWebViewGtkHTML *web_view, - gboolean editable); -gboolean e_web_view_gtkhtml_get_inline_spelling - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_inline_spelling - (EWebViewGtkHTML *web_view, - gboolean inline_spelling); -gboolean e_web_view_gtkhtml_get_magic_links - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_magic_links - (EWebViewGtkHTML *web_view, - gboolean magic_links); -gboolean e_web_view_gtkhtml_get_magic_smileys - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_magic_smileys - (EWebViewGtkHTML *web_view, - gboolean magic_smileys); -const gchar * e_web_view_gtkhtml_get_selected_uri - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_selected_uri - (EWebViewGtkHTML *web_view, - const gchar *selected_uri); -GdkPixbufAnimation * - e_web_view_gtkhtml_get_cursor_image - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_cursor_image - (EWebViewGtkHTML *web_view, - GdkPixbufAnimation *animation); -GtkAction * e_web_view_gtkhtml_get_open_proxy - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_open_proxy - (EWebViewGtkHTML *web_view, - GtkAction *open_proxy); -GtkTargetList * e_web_view_gtkhtml_get_paste_target_list - (EWebViewGtkHTML *web_view); -GtkAction * e_web_view_gtkhtml_get_print_proxy - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_print_proxy - (EWebViewGtkHTML *web_view, - GtkAction *print_proxy); -GtkAction * e_web_view_gtkhtml_get_save_as_proxy - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_set_save_as_proxy - (EWebViewGtkHTML *web_view, - GtkAction *save_as_proxy); -GtkAction * e_web_view_gtkhtml_get_action (EWebViewGtkHTML *web_view, - const gchar *action_name); -GtkActionGroup *e_web_view_gtkhtml_get_action_group - (EWebViewGtkHTML *web_view, - const gchar *group_name); -gchar * e_web_view_gtkhtml_extract_uri (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkHTML *frame); -void e_web_view_gtkhtml_copy_clipboard - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_cut_clipboard - (EWebViewGtkHTML *web_view); -gboolean e_web_view_gtkhtml_is_selection_active - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_paste_clipboard - (EWebViewGtkHTML *web_view); -gboolean e_web_view_gtkhtml_scroll_forward - (EWebViewGtkHTML *web_view); -gboolean e_web_view_gtkhtml_scroll_backward - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_select_all (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_unselect_all (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_zoom_100 (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_zoom_in (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_zoom_out (EWebViewGtkHTML *web_view); -GtkUIManager * e_web_view_gtkhtml_get_ui_manager - (EWebViewGtkHTML *web_view); -GtkWidget * e_web_view_gtkhtml_get_popup_menu - (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_show_popup_menu - (EWebViewGtkHTML *web_view, - GdkEventButton *event, - GtkMenuPositionFunc func, - gpointer user_data); -void e_web_view_gtkhtml_status_message - (EWebViewGtkHTML *web_view, - const gchar *status_message); -void e_web_view_gtkhtml_stop_loading (EWebViewGtkHTML *web_view); -void e_web_view_gtkhtml_update_actions - (EWebViewGtkHTML *web_view); - -G_END_DECLS - -#endif /* E_WEB_VIEW_GTKHTML_H */ diff --git a/e-util/e-web-view.c b/e-util/e-web-view.c index afb3cc9fef..0b83d81a34 100644 --- a/e-util/e-web-view.c +++ b/e-util/e-web-view.c @@ -359,8 +359,6 @@ static void web_view_init_web_settings (WebKitWebView *web_view) { WebKitWebSettings *web_settings; - GObjectClass *class; - GParamSpec *pspec; web_settings = webkit_web_settings_new (); @@ -373,19 +371,9 @@ web_view_init_web_settings (WebKitWebView *web_view) "enable-offline-web-application-cache", FALSE, "enable-site-specific-quirks", TRUE, "enable-scripts", FALSE, + "respect-image-orientation", TRUE, NULL); - /* This property was introduced in WebKitGTK 2.0, - * so check for it and enable it if it's present. */ - class = G_OBJECT_GET_CLASS (web_settings); - pspec = g_object_class_find_property ( - class, "respect-image-orientation"); - if (pspec != NULL) { - g_object_set ( - G_OBJECT (web_settings), - pspec->name, TRUE, NULL); - } - g_object_bind_property ( web_settings, "enable-caret-browsing", web_view, "caret-mode", @@ -3377,9 +3365,9 @@ e_web_view_install_request_handler (EWebView *web_view, soup_session_add_feature_by_type (session, handler_type); } -static void -create_and_add_css_style_sheet (WebKitDOMDocument *document, - const gchar *style_sheet_id) +void +e_web_view_create_and_add_css_style_sheet (WebKitDOMDocument *document, + const gchar *style_sheet_id) { WebKitDOMElement *style_element; @@ -3388,15 +3376,9 @@ create_and_add_css_style_sheet (WebKitDOMDocument *document, if (!style_element) { /* Create new <style> element */ style_element = webkit_dom_document_create_element (document, "style", NULL); -#if WEBKIT_CHECK_VERSION(2,2,0) /* XXX should really be (2,1,something) */ webkit_dom_element_set_id ( WEBKIT_DOM_ELEMENT (style_element), style_sheet_id); -#else - webkit_dom_html_element_set_id ( - WEBKIT_DOM_HTML_ELEMENT (style_element), - style_sheet_id); -#endif webkit_dom_html_style_element_set_media ( WEBKIT_DOM_HTML_STYLE_ELEMENT (style_element), "screen"); @@ -3427,7 +3409,7 @@ add_css_rule_into_style_sheet (WebKitDOMDocument *document, style_element = webkit_dom_document_get_element_by_id (document, style_sheet_id); if (!style_element) { - create_and_add_css_style_sheet (document, style_sheet_id); + e_web_view_create_and_add_css_style_sheet (document, style_sheet_id); style_element = webkit_dom_document_get_element_by_id (document, style_sheet_id); } @@ -3551,3 +3533,125 @@ e_web_view_add_css_rule_into_style_sheet (EWebView *view, selector, style); } + +gboolean +element_has_id (WebKitDOMElement *element, + const gchar* id) +{ + gchar *element_id; + + if (!element) + return FALSE; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) + return FALSE; + + element_id = webkit_dom_element_get_id (element); + + if (g_ascii_strcasecmp (element_id, id) != 0) { + g_free (element_id); + return FALSE; + } + g_free (element_id); + + return TRUE; +} + +gboolean +element_has_tag (WebKitDOMElement *element, + const gchar* tag) +{ + gchar *element_tag; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) + return FALSE; + + element_tag = webkit_dom_node_get_local_name (WEBKIT_DOM_NODE (element)); + + if (g_ascii_strcasecmp (element_tag, tag) != 0) { + g_free (element_tag); + return FALSE; + } + g_free (element_tag); + + return TRUE; +} + +gboolean +element_has_class (WebKitDOMElement *element, + const gchar* class) +{ + gchar *element_class; + + if (!element) + return FALSE; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) + return FALSE; + + element_class = webkit_dom_element_get_class_name (element); + + if (g_strstr_len (element_class, -1, class)) { + g_free (element_class); + return TRUE; + } + g_free (element_class); + + return FALSE; +} + +void +element_add_class (WebKitDOMElement *element, + const gchar* class) +{ + gchar *element_class; + gchar *new_class; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) + return; + + if (element_has_class (element, class)) + return; + + element_class = webkit_dom_element_get_class_name (element); + + if (g_strcmp0 (element_class, "") == 0) + new_class = g_strdup (class); + else + new_class = g_strconcat (element_class, " ", class, NULL); + + webkit_dom_element_set_class_name (element, new_class); + + g_free (element_class); + g_free (new_class); +} + +void +element_remove_class (WebKitDOMElement *element, + const gchar* class) +{ + gchar *element_class; + GString *result; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) + return; + + if (!element_has_class (element, class)) + return; + + element_class = webkit_dom_element_get_class_name (element); + + if (g_strcmp0 (element_class, class) == 0) { + webkit_dom_element_remove_attribute (element, "class"); + g_free (element_class); + return; + } + + result = e_str_replace_string (element_class, class, ""); + if (result) { + webkit_dom_element_set_class_name (element, result->str); + g_string_free (result, TRUE); + } + + g_free (element_class); +} diff --git a/e-util/e-web-view.h b/e-util/e-web-view.h index 62bd045f8a..cbfd2cefad 100644 --- a/e-util/e-web-view.h +++ b/e-util/e-web-view.h @@ -197,11 +197,24 @@ GInputStream * e_web_view_request_finish (EWebView *web_view, void e_web_view_install_request_handler (EWebView *web_view, GType handler_type); +void e_web_view_create_and_add_css_style_sheet + (WebKitDOMDocument* document, + const gchar *style_sheet_id); void e_web_view_add_css_rule_into_style_sheet (EWebView *web_view, const gchar *style_sheet_id, const gchar *selector, const gchar *style); +gboolean element_has_id (WebKitDOMElement *element, + const gchar* id); +gboolean element_has_tag (WebKitDOMElement *element, + const gchar* tag); +gboolean element_has_class (WebKitDOMElement *element, + const gchar* class); +void element_add_class (WebKitDOMElement *element, + const gchar* class); +void element_remove_class (WebKitDOMElement *element, + const gchar* class); G_END_DECLS #endif /* E_WEB_VIEW_H */ diff --git a/e-util/test-html-editor.c b/e-util/test-html-editor.c new file mode 100644 index 0000000000..488eb44bf1 --- /dev/null +++ b/e-util/test-html-editor.c @@ -0,0 +1,497 @@ +/* + * e-html-editor-test.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 the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include <gtk/gtk.h> +#include <e-util/e-util.h> + +#include <glib/gi18n-lib.h> + +static const gchar *file_ui = +"<ui>\n" +" <menubar name='main-menu'>\n" +" <menu action='file-menu'>\n" +" <menuitem action='save'/>\n" +" <menuitem action='save-as'/>\n" +" <separator/>\n" +" <menuitem action='print-preview'/>\n" +" <menuitem action='print'/>\n" +" <separator/>\n" +" <menuitem action='disable-editor'/>\n" +" <separator/>\n" +" <menuitem action='quit'/>\n" +" </menu>\n" +" </menubar>\n" +"</ui>"; + +static const gchar *view_ui = +"<ui>\n" +" <menubar name='main-menu'>\n" +" <menu action='view-menu'>\n" +" <menuitem action='view-html-output'/>\n" +" <menuitem action='view-html-source'/>\n" +" <menuitem action='view-plain-source'/>\n" +" <menuitem action='view-inspector'/>\n" +" </menu>\n" +" </menubar>\n" +"</ui>"; + +static void +handle_error (GError **error) +{ + if (*error != NULL) { + g_warning ("%s", (*error)->message); + g_clear_error (error); + } +} + +static GtkPrintOperationResult +print (EHTMLEditor *editor, + GtkPrintOperationAction action) +{ + WebKitWebFrame *frame; + GtkPrintOperation *operation; + GtkPrintOperationResult result; + GError *error = NULL; + + operation = gtk_print_operation_new (); + + frame = webkit_web_view_get_main_frame ( + WEBKIT_WEB_VIEW (e_html_editor_get_view (editor))); + result = webkit_web_frame_print_full (frame, operation, action, NULL); + + g_object_unref (operation); + handle_error (&error); + + return result; +} + +static gint +save_dialog (EHTMLEditor *editor) +{ + GtkWidget *dialog; + const gchar *filename; + gint response; + + dialog = gtk_file_chooser_dialog_new ( + _("Save As"), + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (editor))), + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, + NULL); + + gtk_file_chooser_set_do_overwrite_confirmation ( + GTK_FILE_CHOOSER (dialog), TRUE); + + filename = e_html_editor_get_filename (editor); + + if (filename != NULL) + gtk_file_chooser_set_filename ( + GTK_FILE_CHOOSER (dialog), filename); + else + gtk_file_chooser_set_current_name ( + GTK_FILE_CHOOSER (dialog), _("Untitled document")); + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == GTK_RESPONSE_ACCEPT) { + gchar *new_filename; + + new_filename = gtk_file_chooser_get_filename ( + GTK_FILE_CHOOSER (dialog)); + e_html_editor_set_filename (editor, new_filename); + g_free (new_filename); + } + + gtk_widget_destroy (dialog); + + return response; +} + +static void +view_source_dialog (EHTMLEditor *editor, + const gchar *title, + gboolean plain_text, + gboolean show_source) +{ + GtkWidget *dialog; + GtkWidget *content; + GtkWidget *content_area; + GtkWidget *scrolled_window; + gchar * html; + + dialog = gtk_dialog_new_with_buttons ( + title, + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (editor))), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); + gtk_box_pack_start ( + GTK_BOX (content_area), + scrolled_window, TRUE, TRUE, 0); + + gtk_container_set_border_width (GTK_CONTAINER (dialog), 6); + gtk_container_set_border_width (GTK_CONTAINER (scrolled_window), 6); + gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 300); + + if (plain_text) { + html = e_html_editor_view_get_text_plain ( + e_html_editor_get_view (editor)); + } else { + html = e_html_editor_view_get_text_html ( + e_html_editor_get_view (editor)); + } + + if (show_source || plain_text) { + GtkTextBuffer *buffer; + + content = gtk_text_view_new (); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (content)); + gtk_text_buffer_set_text (buffer, html, -1); + gtk_text_view_set_editable (GTK_TEXT_VIEW (content), FALSE); + } else { + content = webkit_web_view_new (); + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (content), html, NULL, NULL, NULL); + } + g_free (html); + + gtk_container_add (GTK_CONTAINER (scrolled_window), content); + gtk_widget_show_all (scrolled_window); + + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static void +action_print_cb (GtkAction *action, + EHTMLEditor *editor) +{ + print (editor, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG); +} + +static void +action_print_preview_cb (GtkAction *action, + EHTMLEditor *editor) +{ + print (editor, GTK_PRINT_OPERATION_ACTION_PREVIEW); +} + +static void +action_quit_cb (GtkAction *action, + EHTMLEditor *editor) +{ + gtk_main_quit (); +} + +static void +action_save_cb (GtkAction *action, + EHTMLEditor *editor) +{ + const gchar *filename; + gboolean as_html; + GError *error = NULL; + + if (e_html_editor_get_filename (editor) == NULL) + if (save_dialog (editor) == GTK_RESPONSE_CANCEL) + return; + + filename = e_html_editor_get_filename (editor); + as_html = (e_html_editor_view_get_html_mode (e_html_editor_get_view (editor))); + + e_html_editor_save (editor, filename, as_html, &error); + handle_error (&error); +} + +static void +action_save_as_cb (GtkAction *action, + EHTMLEditor *editor) +{ + const gchar *filename; + gboolean as_html; + GError *error = NULL; + + if (save_dialog (editor) == GTK_RESPONSE_CANCEL) + return; + + filename = e_html_editor_get_filename (editor); + as_html = (e_html_editor_view_get_html_mode (e_html_editor_get_view (editor))); + + e_html_editor_save (editor, filename, as_html, &error); + handle_error (&error); +} + +static void +action_toggle_editor (GtkAction *action, + EHTMLEditor *editor) +{ + EHTMLEditorView *view; + + view = e_html_editor_get_view (editor); + webkit_web_view_set_editable ( + WEBKIT_WEB_VIEW (view), + ! webkit_web_view_get_editable (WEBKIT_WEB_VIEW (view))); +} + +static void +action_view_html_output (GtkAction *action, + EHTMLEditor *editor) +{ + view_source_dialog (editor, _("HTML Output"), FALSE, FALSE); +} + +static void +action_view_html_source (GtkAction *action, + EHTMLEditor *editor) +{ + view_source_dialog (editor, _("HTML Source"), FALSE, TRUE); +} + +static void +action_view_plain_source (GtkAction *action, + EHTMLEditor *editor) +{ + view_source_dialog (editor, _("Plain Source"), TRUE, FALSE); +} + +static void +action_view_inspector (GtkAction *action, + EHTMLEditor *editor) +{ + WebKitWebInspector *inspector; + EHTMLEditorView *view; + + view = e_html_editor_get_view (editor); + inspector = webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (view)); + + webkit_web_inspector_show (inspector); +} + +static GtkActionEntry file_entries[] = { + + { "print", + GTK_STOCK_PRINT, + N_("_Print..."), + NULL, + NULL, + G_CALLBACK (action_print_cb) }, + + { "print-preview", + GTK_STOCK_PRINT_PREVIEW, + N_("Print Pre_view"), + NULL, + NULL, + G_CALLBACK (action_print_preview_cb) }, + + { "quit", + GTK_STOCK_QUIT, + N_("_Quit"), + NULL, + NULL, + G_CALLBACK (action_quit_cb) }, + + { "save", + GTK_STOCK_SAVE, + N_("_Save"), + NULL, + NULL, + G_CALLBACK (action_save_cb) }, + + { "save-as", + GTK_STOCK_SAVE_AS, + N_("Save _As..."), + NULL, + NULL, + G_CALLBACK (action_save_as_cb) }, + + { "disable-editor", + NULL, + N_("Disable/Enable Editor"), + NULL, + NULL, + G_CALLBACK (action_toggle_editor) }, + + { "file-menu", + NULL, + N_("_File"), + NULL, + NULL, + NULL } +}; + +static GtkActionEntry view_entries[] = { + + { "view-html-output", + NULL, + N_("HTML _Output"), + NULL, + NULL, + G_CALLBACK (action_view_html_output) }, + + { "view-html-source", + NULL, + N_("_HTML Source"), + NULL, + NULL, + G_CALLBACK (action_view_html_source) }, + + { "view-plain-source", + NULL, + N_("_Plain Source"), + NULL, + NULL, + G_CALLBACK (action_view_plain_source) }, + + { "view-inspector", + NULL, + N_("Inspector"), + NULL, + NULL, + G_CALLBACK (action_view_inspector) }, + + { "view-menu", + NULL, + N_("_View"), + NULL, + NULL, + NULL } +}; + +static WebKitWebView * +open_inspector (WebKitWebInspector *inspector, + WebKitWebView *webview, + gpointer user_data) +{ + GtkWidget *window; + GtkWidget *inspector_view; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + inspector_view = webkit_web_view_new (); + + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (inspector_view)); + + gtk_widget_set_size_request (window, 600, 480); + gtk_widget_show (window); + + return WEBKIT_WEB_VIEW (inspector_view); +} + +gint +main (gint argc, + gchar **argv) +{ + GtkActionGroup *action_group; + GtkUIManager *manager; + GtkWidget *container; + GtkWidget *widget; + EHTMLEditor *editor; + EHTMLEditorView *view; + WebKitWebInspector *inspector; + + GError *error = NULL; + + bindtextdomain (GETTEXT_PACKAGE, EVOLUTION_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + gtk_init (&argc, &argv); + + editor = g_object_ref_sink (e_html_editor_new ()); + view = e_html_editor_get_view (editor); + + inspector = webkit_web_view_get_inspector ( + WEBKIT_WEB_VIEW (view)); + g_signal_connect ( + inspector, "inspect-web-view", + G_CALLBACK (open_inspector), NULL); + + widget = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_widget_set_size_request (widget, 600, 400); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "destroy", + G_CALLBACK (gtk_main_quit), NULL); + + container = widget; + + widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + container = widget; + + widget = e_html_editor_get_managed_widget (editor, "/main-menu"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = e_html_editor_get_managed_widget (editor, "/main-toolbar"); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = GTK_WIDGET (editor); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + manager = e_html_editor_get_ui_manager (editor); + + gtk_ui_manager_add_ui_from_string (manager, file_ui, -1, &error); + handle_error (&error); + + gtk_ui_manager_add_ui_from_string (manager, view_ui, -1, &error); + handle_error (&error); + + action_group = gtk_action_group_new ("file"); + gtk_action_group_set_translation_domain ( + action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions ( + action_group, file_entries, + G_N_ELEMENTS (file_entries), editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + action_group = gtk_action_group_new ("view"); + gtk_action_group_set_translation_domain ( + action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions ( + action_group, view_entries, + G_N_ELEMENTS (view_entries), editor); + gtk_ui_manager_insert_action_group (manager, action_group, 0); + + gtk_ui_manager_ensure_update (manager); + + g_signal_connect ( + editor, "destroy", + G_CALLBACK (gtk_main_quit), NULL); + + gtk_main (); + + g_object_unref (editor); + + return 0; +} |