[evolution] Bug 757939 - Undo failure after dragging text



commit 1458f8a01e5904e6b69b6dd4194b44ec565ce226
Author: Tomas Popela <tpopela redhat com>
Date:   Thu Dec 3 15:26:17 2015 +0100

    Bug 757939 - Undo failure after dragging text
    
    The problem was that after we started to rely on WebKit while doing drag and drop
    within the view we were not saving the history at all. This change implements
    the history saving for drag and drop and fixes various problems related to drag
    and drop as sometimes the content was copied instead of moved and sometimes the
    dropped content (from outside of composer) was inserted twice.

 composer/e-composer-private.c |    1 +
 composer/e-composer-private.h |    1 +
 composer/e-msg-composer.c     |  286 +++++++++++++++++++++++++++++++++++++----
 e-util/e-html-editor-view.c   |  127 ++++++++++++++++--
 e-util/e-html-editor-view.h   |    2 +
 5 files changed, 379 insertions(+), 38 deletions(-)
---
diff --git a/composer/e-composer-private.c b/composer/e-composer-private.c
index b57c561..dfe1597 100644
--- a/composer/e-composer-private.c
+++ b/composer/e-composer-private.c
@@ -141,6 +141,7 @@ e_composer_private_constructed (EMsgComposer *composer)
        priv->dnd_is_uri = FALSE;
        priv->check_if_signature_is_changed = FALSE;
        priv->ignore_next_signature_change = FALSE;
+       priv->dnd_history_saved = FALSE;
 
        priv->focused_entry = NULL;
 
diff --git a/composer/e-composer-private.h b/composer/e-composer-private.h
index a87a842..bf9a1ef 100644
--- a/composer/e-composer-private.h
+++ b/composer/e-composer-private.h
@@ -105,6 +105,7 @@ struct _EMsgComposerPrivate {
        gboolean check_if_signature_is_changed;
        gboolean ignore_next_signature_change;
        gboolean is_sending_message;
+       gboolean dnd_history_saved;
 
        gint focused_entry_selection_start;
        gint focused_entry_selection_end;
diff --git a/composer/e-msg-composer.c b/composer/e-msg-composer.c
index acfdeb7..728310b 100644
--- a/composer/e-msg-composer.c
+++ b/composer/e-msg-composer.c
@@ -1821,6 +1821,51 @@ msg_composer_drag_motion_cb (GtkWidget *widget,
        return FALSE;
 }
 
+static void
+insert_nbsp_history_event (EHTMLEditorView *editor_view,
+                           gboolean delete,
+                           guint x,
+                           guint y)
+{
+       EHTMLEditorViewHistoryEvent *event;
+       WebKitDOMDocument *document;
+       WebKitDOMDocumentFragment *fragment;
+
+       document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (editor_view));
+
+       event = g_new0 (EHTMLEditorViewHistoryEvent, 1);
+       event->type = HISTORY_AND;
+       e_html_editor_view_insert_new_history_event (editor_view, event);
+
+       fragment = webkit_dom_document_create_document_fragment (document);
+       webkit_dom_node_append_child (
+               WEBKIT_DOM_NODE (fragment),
+               WEBKIT_DOM_NODE (
+                       webkit_dom_document_create_text_node (document, UNICODE_NBSP)),
+               NULL);
+
+       event = g_new0 (EHTMLEditorViewHistoryEvent, 1);
+       event->type = HISTORY_DELETE;
+
+       if (delete)
+               g_object_set_data (
+                       G_OBJECT (fragment), "-x-evo-delete-key", GINT_TO_POINTER (1));
+
+       event->data.fragment = fragment;
+
+       event->before.start.x = x;
+       event->before.start.y = y;
+       event->before.end.x = x;
+       event->before.end.y = y;
+
+       event->after.start.x = x;
+       event->after.start.y = y;
+       event->after.end.x = x;
+       event->after.end.y = y;
+
+       e_html_editor_view_insert_new_history_event (editor_view, event);
+}
+
 static gboolean
 msg_composer_drag_drop_cb (GtkWidget *widget,
                            GdkDragContext *context,
@@ -1832,30 +1877,228 @@ msg_composer_drag_drop_cb (GtkWidget *widget,
        GdkAtom target;
        GtkWidget *source_widget;
 
-       /* When we are doind DnD just inside the web view, the DnD is supposed
+       /* When we are doing DnD just inside the web view, the DnD is supposed
         * to move things around. */
        source_widget = gtk_drag_get_source_widget (context);
        if (E_IS_HTML_EDITOR_VIEW (source_widget)) {
                EHTMLEditor *editor = e_msg_composer_get_editor (composer);
                EHTMLEditorView *editor_view = e_html_editor_get_view (editor);
 
-               if ((gpointer) editor_view == (gpointer) source_widget)
+               if ((gpointer) editor_view == (gpointer) source_widget) {
+                       EHTMLEditorSelection *selection;
+                       EHTMLEditorViewHistoryEvent *event;
+                       gboolean start_to_start, end_to_end;
+                       gchar *range_text;
+                       guint x, y;
+                       WebKitDOMDocument *document;
+                       WebKitDOMDocumentFragment *fragment;
+                       WebKitDOMDOMSelection *dom_selection;
+                       WebKitDOMDOMWindow *dom_window;
+                       WebKitDOMRange *beginning_of_line = NULL;
+                       WebKitDOMRange *range = NULL, *range_clone = NULL;
+
+                       selection = e_html_editor_view_get_selection (editor_view);
+                       document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (editor_view));
+
+                       if (!(dom_window = webkit_dom_document_get_default_view (document)))
+                               return FALSE;
+
+                       if (!(dom_selection = webkit_dom_dom_window_get_selection (dom_window))) {
+                               g_object_unref (dom_window);
+                               return FALSE;
+                       }
+
+                       if (webkit_dom_dom_selection_get_range_count (dom_selection) < 1) {
+                               g_object_unref (dom_selection);
+                               g_object_unref (dom_window);
+                               return FALSE;
+                       }
+
+                       /* Obtain the dragged content. */
+                       range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL);
+                       range_clone = webkit_dom_range_clone_range (range, NULL);
+
+                       /* Create the history event for the content that will
+                        * be removed by DnD. */
+                       event = g_new0 (EHTMLEditorViewHistoryEvent, 1);
+                       event->type = HISTORY_DELETE;
+
+                       e_html_editor_selection_get_selection_coordinates (
+                               selection,
+                               &event->before.start.x,
+                               &event->before.start.y,
+                               &event->before.end.x,
+                               &event->before.end.y);
+
+                       x = event->before.start.x;
+                       y = event->before.start.y;
+
+                       event->after.start.x = x;
+                       event->after.start.y = y;
+                       event->after.end.x = x;
+                       event->after.end.y = y;
+
+                       /* Save the content that will be removed. */
+                       fragment = webkit_dom_range_clone_contents (range_clone, NULL);
+
+                       /* Extend the cloned range to point one character after
+                        * the selection ends to later check if there is a whitespace
+                        * after it. */
+                       webkit_dom_range_set_end (
+                               range_clone,
+                               webkit_dom_range_get_end_container (range_clone, NULL),
+                               webkit_dom_range_get_end_offset (range_clone, NULL) + 1,
+                               NULL);
+                       range_text = webkit_dom_range_get_text (range_clone);
+
+                       /* Check if the current selection starts on the beginning
+                        * of line. */
+                       webkit_dom_dom_selection_modify (
+                               dom_selection, "extend", "left", "lineboundary");
+                       beginning_of_line = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL);
+                       start_to_start = webkit_dom_range_compare_boundary_points (
+                               beginning_of_line, 0 /* START_TO_START */, range, NULL) == 0;
+
+                       /* Restore the selection to state before the check. */
+                       webkit_dom_dom_selection_remove_all_ranges (dom_selection);
+                       webkit_dom_dom_selection_add_range (dom_selection, range);
+                       g_object_unref (beginning_of_line);
+
+                       /* Check if the current selection end on the end of the line. */
+                       webkit_dom_dom_selection_modify (
+                               dom_selection, "extend", "right", "lineboundary");
+                       beginning_of_line = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL);
+                       end_to_end = webkit_dom_range_compare_boundary_points (
+                               beginning_of_line, 2 /* END_TO_END */, range, NULL) == 0;
+
+                       /* Dragging the whole line. */
+                       if (start_to_start && end_to_end) {
+                               WebKitDOMNode *container, *actual_block, *tmp_block;
+
+                               /* Select the whole line (to the beginning of the next
+                                * one so we can reuse the undo code while undoing this.
+                                * Because of this we need to special mark the event
+                                * with -x-evo-drag-and-drop to correct the selection
+                                * after undoing it (otherwise the beginning of the next
+                                * line will be selected as well. */
+                               webkit_dom_dom_selection_modify (
+                                       dom_selection, "extend", "right", "character");
+                               g_object_unref (beginning_of_line);
+                               beginning_of_line = webkit_dom_dom_selection_get_range_at (dom_selection, 0, 
NULL);
+
+                               container = webkit_dom_range_get_end_container (range, NULL);
+                               actual_block = e_html_editor_get_parent_block_node_from_child (container);
+
+                               tmp_block = webkit_dom_range_get_end_container (beginning_of_line, NULL);
+                               if ((tmp_block = e_html_editor_get_parent_block_node_from_child (tmp_block))) 
{
+                                       e_html_editor_selection_get_selection_coordinates (
+                                               selection,
+                                               &event->before.start.x,
+                                               &event->before.start.y,
+                                               &event->before.end.x,
+                                               &event->before.end.y);
+
+                                       /* Create the right content for the history event. */
+                                       fragment = webkit_dom_document_create_document_fragment (document);
+                                       /* The removed line. */
+                                       webkit_dom_node_append_child (
+                                               WEBKIT_DOM_NODE (fragment),
+                                               webkit_dom_node_clone_node (actual_block, TRUE),
+                                               NULL);
+                                       /* The following block, but empty. */
+                                       webkit_dom_node_append_child (
+                                               WEBKIT_DOM_NODE (fragment),
+                                               webkit_dom_node_clone_node (tmp_block, FALSE),
+                                               NULL);
+                                       g_object_set_data (
+                                               G_OBJECT (fragment),
+                                               "-x-evo-drag-and-drop",
+                                               GINT_TO_POINTER (1));
+                                       /* It should act as a Delete key press. */
+                                       g_object_set_data (G_OBJECT (
+                                               fragment),
+                                               "-x-evo-delete-key",
+                                               GINT_TO_POINTER (1));
+                               }
+                       }
+
+                       event->data.fragment = fragment;
+                       e_html_editor_view_insert_new_history_event (editor_view, event);
+
+                       /* Selection is ending on the end of the line, check if
+                        * there is a space before the selection start. If so, it
+                        * will be removed and we need create the history event
+                        * for it. */
+                       if (end_to_end) {
+                               gchar *range_text_start;
+                               glong start_offset;
+
+                               start_offset = webkit_dom_range_get_start_offset (range_clone, NULL);
+                               webkit_dom_range_set_start (
+                                       range_clone,
+                                       webkit_dom_range_get_start_container (range_clone, NULL),
+                                       start_offset > 0 ? start_offset - 1 : 0,
+                                       NULL);
+
+                               range_text_start = webkit_dom_range_get_text (range_clone);
+                               if (g_str_has_prefix (range_text_start, " ") ||
+                                   g_str_has_prefix (range_text_start, UNICODE_NBSP))
+                                       insert_nbsp_history_event (editor_view, FALSE, x, y);
+
+                               g_free (range_text_start);
+                       }
+
+                       /* WebKit removes the space (if presented) after selection and
+                        * we need to create a new history event for it. */
+                       if (g_str_has_suffix (range_text, " ") ||
+                           g_str_has_suffix (range_text, UNICODE_NBSP))
+                               insert_nbsp_history_event (editor_view, TRUE, x, y);
+
+                       g_free (range_text);
+
+                       /* Restore the selection to original state. */
+                       webkit_dom_dom_selection_remove_all_ranges (dom_selection);
+                       webkit_dom_dom_selection_add_range (dom_selection, range);
+                       g_object_unref (beginning_of_line);
+
+                       /* All the things above were about removing the content,
+                        * create an AND event to continue later with inserting
+                        * the dropped content. */
+                       event = g_new0 (EHTMLEditorViewHistoryEvent, 1);
+                       event->type = HISTORY_AND;
+                       e_html_editor_view_insert_new_history_event (editor_view, event);
+
+                       g_object_unref (dom_selection);
+                       g_object_unref (dom_window);
+
+                       g_object_unref (range);
+                       g_object_unref (range_clone);
+
                        return FALSE;
+               }
        }
 
        target = gtk_drag_dest_find_target (widget, context, NULL);
        if (target == GDK_NONE)
                gdk_drag_status (context, 0, time);
        else {
-               /* Prevent WebKit from pasting the URI of file into the view. */
-               if (composer->priv->dnd_is_uri)
+               /* Prevent WebKit from pasting the URI of file into the view. Also
+                * prevent it from inserting the text/plain or text/html content as we
+                * want to insert it ourselves. */
+               if (composer->priv->dnd_is_uri || !E_IS_HTML_EDITOR_VIEW (source_widget))
                        g_signal_stop_emission_by_name (widget, "drag-drop");
 
                composer->priv->dnd_is_uri = FALSE;
 
-               gdk_drag_status (context, GDK_ACTION_COPY, time);
+               if (E_IS_HTML_EDITOR_VIEW (source_widget))
+                       gdk_drag_status (context, GDK_ACTION_MOVE, time);
+               else
+                       gdk_drag_status (context, GDK_ACTION_COPY, time);
+
                composer->priv->drop_occured = TRUE;
                gtk_drag_get_data (widget, context, target, time);
+
+               return TRUE;
        }
 
        return FALSE;
@@ -1873,32 +2116,22 @@ msg_composer_drag_data_received_after_cb (GtkWidget *widget,
 {
        EHTMLEditor *editor;
        EHTMLEditorView *view;
-       WebKitDOMDocument *document;
-       WebKitDOMDOMWindow *dom_window;
-       WebKitDOMDOMSelection *dom_selection;
 
        if (!composer->priv->drop_occured)
-               return;
+               goto out;
 
-       composer->priv->drop_occured = FALSE;
+       /* Save just history for events handled by WebKit. */
+       if (composer->priv->dnd_history_saved)
+               goto out;
 
        editor = e_msg_composer_get_editor (composer);
        view = e_html_editor_get_view (editor);
-
-       document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
-       dom_window = webkit_dom_document_get_default_view (document);
-       dom_selection = webkit_dom_dom_window_get_selection (dom_window);
-
-       /* When text is DnD'ed into the view, WebKit will select it. So let's
-        * collapse it to its end to have the caret after the DnD'ed text. */
-       webkit_dom_dom_selection_collapse_to_end (dom_selection, NULL);
-
-       g_clear_object (&dom_selection);
-       g_clear_object (&dom_window);
-
+       e_html_editor_view_save_history_for_drop (view);
        e_html_editor_view_check_magic_links (view, FALSE);
-       /* Also force spell check on view. */
-       e_html_editor_view_force_spell_check_in_viewport (view);
+
+ out:
+       composer->priv->drop_occured = FALSE;
+       composer->priv->dnd_history_saved = FALSE;
 }
 
 static gchar *
@@ -1947,7 +2180,9 @@ msg_composer_drag_data_received_cb (GtkWidget *widget,
        html_mode = e_html_editor_view_get_html_mode (html_editor_view);
        editor_selection = e_html_editor_view_get_selection (html_editor_view);
 
-       /* When we are doind DnD just inside the web view, the DnD is supposed
+       composer->priv->dnd_history_saved = TRUE;
+
+       /* When we are doing DnD just inside the web view, the DnD is supposed
         * to move things around. */
        source_widget = gtk_drag_get_source_widget (context);
        if (E_IS_HTML_EDITOR_VIEW (source_widget)) {
@@ -1991,6 +2226,7 @@ msg_composer_drag_data_received_cb (GtkWidget *widget,
            info == DND_TARGET_TYPE_STRING ||
            info == DND_TARGET_TYPE_TEXT_PLAIN ||
            info == DND_TARGET_TYPE_TEXT_PLAIN_UTF8) {
+               composer->priv->dnd_history_saved = FALSE;
                gdk_drag_status (context, 0, time);
                return;
        }
diff --git a/e-util/e-html-editor-view.c b/e-util/e-html-editor-view.c
index 700ba67..964c877 100644
--- a/e-util/e-html-editor-view.c
+++ b/e-util/e-html-editor-view.c
@@ -211,18 +211,13 @@ html_editor_view_get_dom_range (EHTMLEditorView *view)
        return range;
 }
 
-#if d(1)+0
-static void
-print_node_inner_html (WebKitDOMNode *node)
+static gchar *
+get_node_inner_html (WebKitDOMNode *node)
 {
+       gchar *inner_html;
        WebKitDOMDocument *document;
        WebKitDOMElement *div;
-       gchar *inner_html;
 
-       if (!node) {
-               printf ("    none\n");
-               return;
-       }
        document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (node));
        div = webkit_dom_document_create_element (document, "div", NULL);
        webkit_dom_node_append_child (
@@ -233,6 +228,22 @@ print_node_inner_html (WebKitDOMNode *node)
        inner_html = webkit_dom_html_element_get_inner_html (WEBKIT_DOM_HTML_ELEMENT (div));
        remove_node (WEBKIT_DOM_NODE (div));
 
+       return inner_html;
+}
+
+#if d(1)+0
+static void
+print_node_inner_html (WebKitDOMNode *node)
+{
+       gchar *inner_html;
+
+       if (!node) {
+               printf ("    none\n");
+               return;
+       }
+
+       inner_html = get_node_inner_html (node);
+
        printf ("    '%s'\n", inner_html);
 
        g_free (inner_html);
@@ -10926,15 +10937,17 @@ e_html_editor_view_set_html_mode (EHTMLEditorView *view,
        g_object_notify (G_OBJECT (view), "html-mode");
 }
 
-static void
-html_editor_view_drag_end_cb (EHTMLEditorView *view,
-                              GdkDragContext *context)
+void
+e_html_editor_view_save_history_for_drop (EHTMLEditorView *view)
 {
+       EHTMLEditorViewHistoryEvent *event;
        gint ii, length;
        WebKitDOMDocument *document;
-       WebKitDOMDOMWindow *dom_window;
+       WebKitDOMDocumentFragment *fragment;
        WebKitDOMDOMSelection *dom_selection;
+       WebKitDOMDOMWindow *dom_window;
        WebKitDOMNodeList *list;
+       WebKitDOMRange *range;
 
        document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
 
@@ -10964,18 +10977,94 @@ html_editor_view_drag_end_cb (EHTMLEditorView *view,
         * lets collapse the selection to have the caret right after the image. */
        dom_window = webkit_dom_document_get_default_view (document);
        dom_selection = webkit_dom_dom_window_get_selection (dom_window);
+
+       range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL);
+
+       /* Remove the last inserted history event as this one was inserted in
+        * body_input_event_cb and is wrong as its type is HISTORY_INPUT. */
+       /* FIXME we could probably disable the HTML input event callback while
+        * doing DnD within the view */
+       if (((EHTMLEditorViewHistoryEvent *) (view->priv->history->data))->type == HISTORY_INPUT)
+               remove_history_event (view, view->priv->history);
+
+       event = g_new0 (EHTMLEditorViewHistoryEvent, 1);
+       event->type = HISTORY_INSERT_HTML;
+
+       /* Get the dropped content. It's easy as it is selected by WebKit. */
+       fragment = webkit_dom_range_clone_contents (range, NULL);
+       event->data.string.from = NULL;
+       /* Get the HTML content of the dropped content. */
+       event->data.string.to = get_node_inner_html (WEBKIT_DOM_NODE (fragment));
+
+       e_html_editor_selection_get_selection_coordinates (
+               view->priv->selection,
+               &event->before.start.x,
+               &event->before.start.y,
+               &event->before.end.x,
+               &event->before.end.y);
+
+       event->before.end.x = event->before.start.x;
+       event->before.end.y = event->before.start.y;
+
        if (length > 0)
                webkit_dom_dom_selection_collapse_to_start (dom_selection, NULL);
        else
                webkit_dom_dom_selection_collapse_to_end (dom_selection, NULL);
 
+       e_html_editor_selection_get_selection_coordinates (
+               view->priv->selection,
+               &event->after.start.x,
+               &event->after.start.y,
+               &event->after.end.x,
+               &event->after.end.y);
+
+       e_html_editor_view_insert_new_history_event (view, event);
+
+       if (!view->priv->html_mode) {
+               WebKitDOMNodeList *list;
+               gint ii, length;
+
+               list = webkit_dom_document_query_selector_all (
+                       document, "span[style^=font-family]", NULL);
+               length = webkit_dom_node_list_get_length (list);
+               if (length > 0)
+                       e_html_editor_selection_save (view->priv->selection);
+
+               for (ii = 0; ii < length; ii++) {
+                       WebKitDOMNode *span, *child;
+
+                       span = webkit_dom_node_list_item (list, ii);
+                       while ((child = webkit_dom_node_get_first_child (span)))
+                               webkit_dom_node_insert_before (
+                                       webkit_dom_node_get_parent_node (span),
+                                       child,
+                                       span,
+                                       NULL);
+
+                       remove_node (span);
+                       g_object_unref (span);
+               }
+               g_object_unref (list);
+
+               if (length > 0)
+                       e_html_editor_selection_restore (view->priv->selection);
+       }
+
        e_html_editor_view_force_spell_check_in_viewport (view);
 
+       g_object_unref (range);
        g_object_unref (dom_selection);
        g_object_unref (dom_window);
 }
 
 static void
+html_editor_view_drag_end_cb (EHTMLEditorView *view,
+                              GdkDragContext *context)
+{
+       e_html_editor_view_save_history_for_drop (view);
+}
+
+static void
 im_context_preedit_start_cb (GtkIMContext *context,
                              EHTMLEditorView *view)
 {
@@ -12915,6 +13004,18 @@ undo_delete (EHTMLEditorView *view,
 
                merge_siblings_if_necessary (document, event->data.fragment);
 
+               /* If undoing drag and drop where the whole line was moved we need
+                * to correct the selection. */
+               if (g_object_get_data (G_OBJECT (event->data.fragment), "-x-evo-drag-and-drop") &&
+                   (element = webkit_dom_document_get_element_by_id (document, 
"-x-evo-selection-end-marker"))) {
+                       WebKitDOMNode *prev_block;
+
+                       prev_block = webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element));
+                       if ((prev_block = webkit_dom_node_get_previous_sibling (prev_block)))
+                               webkit_dom_node_append_child (
+                                       prev_block, WEBKIT_DOM_NODE (element), NULL);
+               }
+
                e_html_editor_selection_restore (selection);
                e_html_editor_view_force_spell_check_in_viewport (view);
        } else {
@@ -12924,7 +13025,7 @@ undo_delete (EHTMLEditorView *view,
 
                element = webkit_dom_document_create_element (document, "span", NULL);
 
-               /* Create temporary node on the selection where the delete occured. */
+               /* Create temporary node on the selection where the delete occurred. */
                if (webkit_dom_document_fragment_query_selector (event->data.fragment, ".Apple-tab-span", 
NULL))
                        range = get_range_for_point (document, event->before.start);
                else
diff --git a/e-util/e-html-editor-view.h b/e-util/e-html-editor-view.h
index 818f487..83813dc 100644
--- a/e-util/e-html-editor-view.h
+++ b/e-util/e-html-editor-view.h
@@ -176,6 +176,8 @@ gboolean    e_html_editor_view_get_html_mode
 void           e_html_editor_view_set_html_mode
                                                (EHTMLEditorView *view,
                                                 gboolean html_mode);
+void           e_html_editor_view_save_history_for_drop
+                                               (EHTMLEditorView *view);
 gboolean       e_html_editor_view_get_inline_spelling
                                                (EHTMLEditorView *view);
 void           e_html_editor_view_set_inline_spelling


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]