Rubber stamped by Sam Weinig.
[WebKit-https.git] / WebKit / gtk / WebCoreSupport / EditorClientGtk.cpp
1 /*
2  *  Copyright (C) 2007 Alp Toker <alp@atoker.com>
3  *  Copyright (C) 2008 Nuanti Ltd.
4  *  Copyright (C) 2009 Diego Escalante Urrelo <diegoe@gnome.org>
5  *  Copyright (C) 2006, 2007 Apple Inc.  All rights reserved.
6  *  Copyright (C) 2009, Igalia S.L.
7  *  Copyright (C) 2010, Martin Robinson <mrobinson@webkit.org>
8  *
9  *  This library is free software; you can redistribute it and/or
10  *  modify it under the terms of the GNU Lesser General Public
11  *  License as published by the Free Software Foundation; either
12  *  version 2 of the License, or (at your option) any later version.
13  *
14  *  This library is distributed in the hope that it will be useful,
15  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  *  Lesser General Public License for more details.
18  *
19  *  You should have received a copy of the GNU Lesser General Public
20  *  License along with this library; if not, write to the Free Software
21  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22  */
23
24 #include "config.h"
25 #include "EditorClientGtk.h"
26
27 #include "DataObjectGtk.h"
28 #include "EditCommand.h"
29 #include "Editor.h"
30 #include <enchant.h>
31 #include "EventNames.h"
32 #include "FocusController.h"
33 #include "Frame.h"
34 #include <glib.h>
35 #include "KeyboardEvent.h"
36 #include "NotImplemented.h"
37 #include "Page.h"
38 #include "PasteboardHelperGtk.h"
39 #include "PlatformKeyboardEvent.h"
40 #include "WindowsKeyboardCodes.h"
41 #include "markup.h"
42 #include "webkitprivate.h"
43 #include <wtf/text/CString.h>
44
45 // Arbitrary depth limit for the undo stack, to keep it from using
46 // unbounded memory.  This is the maximum number of distinct undoable
47 // actions -- unbroken stretches of typed characters are coalesced
48 // into a single action.
49 #define maximumUndoStackDepth 1000
50
51 using namespace WebCore;
52
53 namespace WebKit {
54
55 static void imContextCommitted(GtkIMContext* context, const gchar* compositionString, EditorClient* client)
56 {
57     Frame* frame = core(client->webView())->focusController()->focusedOrMainFrame();
58     if (!frame || !frame->editor()->canEdit())
59         return;
60
61     // If this signal fires during a keydown event when we are not in the middle
62     // of a composition, then treat this 'commit' as a normal key event and just
63     // change the editable area right before the keypress event.
64     if (client->treatContextCommitAsKeyEvent()) {
65         client->updatePendingComposition(compositionString);
66         return;
67     }
68
69     frame->editor()->confirmComposition(String::fromUTF8(compositionString));
70     client->clearPendingComposition();
71 }
72
73 static void imContextPreeditChanged(GtkIMContext* context, EditorClient* client)
74 {
75     Frame* frame = core(client->webView())->focusController()->focusedOrMainFrame();
76     if (!frame || !frame->editor()->canEdit())
77         return;
78
79     // We ignore the provided PangoAttrList for now.
80     GOwnPtr<gchar> newPreedit(0);
81     gtk_im_context_get_preedit_string(context, &newPreedit.outPtr(), 0, 0);
82
83     String preeditString = String::fromUTF8(newPreedit.get());
84     Vector<CompositionUnderline> underlines;
85     underlines.append(CompositionUnderline(0, preeditString.length(), Color(0, 0, 0), false));
86     frame->editor()->setComposition(preeditString, underlines, 0, 0);
87 }
88
89 void EditorClient::updatePendingComposition(const gchar* newComposition)
90 {
91     // The IMContext may signal more than one completed composition in a row,
92     // in which case we want to append them, rather than overwrite the old one.
93     if (!m_pendingComposition)
94         m_pendingComposition.set(g_strdup(newComposition));
95     else
96         m_pendingComposition.set(g_strconcat(m_pendingComposition.get(), newComposition, NULL));
97 }
98
99 void EditorClient::setInputMethodState(bool active)
100 {
101     WebKitWebViewPrivate* priv = m_webView->priv;
102
103     if (active)
104         gtk_im_context_focus_in(priv->imContext);
105     else
106         gtk_im_context_focus_out(priv->imContext);
107
108 #ifdef MAEMO_CHANGES
109     if (active)
110         hildon_gtk_im_context_show(priv->imContext);
111     else
112         hildon_gtk_im_context_hide(priv->imContext);
113 #endif
114 }
115
116 bool EditorClient::shouldDeleteRange(Range*)
117 {
118     notImplemented();
119     return true;
120 }
121
122 bool EditorClient::shouldShowDeleteInterface(HTMLElement*)
123 {
124     return false;
125 }
126
127 bool EditorClient::isContinuousSpellCheckingEnabled()
128 {
129     WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView);
130
131     gboolean enabled;
132     g_object_get(settings, "enable-spell-checking", &enabled, NULL);
133
134     return enabled;
135 }
136
137 bool EditorClient::isGrammarCheckingEnabled()
138 {
139     notImplemented();
140     return false;
141 }
142
143 int EditorClient::spellCheckerDocumentTag()
144 {
145     notImplemented();
146     return 0;
147 }
148
149 bool EditorClient::shouldBeginEditing(WebCore::Range*)
150 {
151     clearPendingComposition();
152
153     notImplemented();
154     return true;
155 }
156
157 bool EditorClient::shouldEndEditing(WebCore::Range*)
158 {
159     clearPendingComposition();
160
161     notImplemented();
162     return true;
163 }
164
165 bool EditorClient::shouldInsertText(const String&, Range*, EditorInsertAction)
166 {
167     notImplemented();
168     return true;
169 }
170
171 bool EditorClient::shouldChangeSelectedRange(Range*, Range*, EAffinity, bool)
172 {
173     notImplemented();
174     return true;
175 }
176
177 bool EditorClient::shouldApplyStyle(WebCore::CSSStyleDeclaration*, WebCore::Range*)
178 {
179     notImplemented();
180     return true;
181 }
182
183 bool EditorClient::shouldMoveRangeAfterDelete(WebCore::Range*, WebCore::Range*)
184 {
185     notImplemented();
186     return true;
187 }
188
189 void EditorClient::didBeginEditing()
190 {
191     notImplemented();
192 }
193
194 void EditorClient::respondToChangedContents()
195 {
196     notImplemented();
197 }
198
199 void EditorClient::respondToChangedSelection()
200 {
201     WebKitWebViewPrivate* priv = m_webView->priv;
202     WebCore::Page* corePage = core(m_webView);
203     Frame* targetFrame = corePage->focusController()->focusedOrMainFrame();
204
205     if (!targetFrame)
206         return;
207
208     if (targetFrame->editor()->ignoreCompositionSelectionChange())
209         return;
210
211 #if PLATFORM(X11)
212     GtkClipboard* clipboard = gtk_widget_get_clipboard(GTK_WIDGET(m_webView), GDK_SELECTION_PRIMARY);
213     DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
214
215     if (targetFrame->selection()->isRange()) {
216         dataObject->clear();
217         dataObject->setRange(targetFrame->selection()->toNormalizedRange());
218         pasteboardHelperInstance()->writeClipboardContents(clipboard, m_webView);
219     }
220 #endif
221
222     if (!targetFrame->editor()->hasComposition())
223         return;
224
225     unsigned start;
226     unsigned end;
227     if (!targetFrame->editor()->getCompositionSelection(start, end)) {
228         // gtk_im_context_reset() clears the composition for us.
229         gtk_im_context_reset(priv->imContext);
230         targetFrame->editor()->confirmCompositionWithoutDisturbingSelection();
231     }
232 }
233
234 void EditorClient::didEndEditing()
235 {
236     notImplemented();
237 }
238
239 void EditorClient::didWriteSelectionToPasteboard()
240 {
241     notImplemented();
242 }
243
244 void EditorClient::didSetSelectionTypesForPasteboard()
245 {
246     notImplemented();
247 }
248
249 bool EditorClient::isEditable()
250 {
251     return webkit_web_view_get_editable(m_webView);
252 }
253
254 void EditorClient::registerCommandForUndo(WTF::PassRefPtr<WebCore::EditCommand> command)
255 {
256     if (undoStack.size() == maximumUndoStackDepth)
257         undoStack.removeFirst();
258     if (!m_isInRedo)
259         redoStack.clear();
260     undoStack.append(command);
261 }
262
263 void EditorClient::registerCommandForRedo(WTF::PassRefPtr<WebCore::EditCommand> command)
264 {
265     redoStack.append(command);
266 }
267
268 void EditorClient::clearUndoRedoOperations()
269 {
270     undoStack.clear();
271     redoStack.clear();
272 }
273
274 bool EditorClient::canUndo() const
275 {
276     return !undoStack.isEmpty();
277 }
278
279 bool EditorClient::canRedo() const
280 {
281     return !redoStack.isEmpty();
282 }
283
284 void EditorClient::undo()
285 {
286     if (canUndo()) {
287         RefPtr<WebCore::EditCommand> command(*(--undoStack.end()));
288         undoStack.remove(--undoStack.end());
289         // unapply will call us back to push this command onto the redo stack.
290         command->unapply();
291     }
292 }
293
294 void EditorClient::redo()
295 {
296     if (canRedo()) {
297         RefPtr<WebCore::EditCommand> command(*(--redoStack.end()));
298         redoStack.remove(--redoStack.end());
299
300         ASSERT(!m_isInRedo);
301         m_isInRedo = true;
302         // reapply will call us back to push this command onto the undo stack.
303         command->reapply();
304         m_isInRedo = false;
305     }
306 }
307
308 bool EditorClient::shouldInsertNode(Node*, Range*, EditorInsertAction)
309 {
310     notImplemented();
311     return true;
312 }
313
314 void EditorClient::pageDestroyed()
315 {
316     delete this;
317 }
318
319 bool EditorClient::smartInsertDeleteEnabled()
320 {
321     notImplemented();
322     return false;
323 }
324
325 bool EditorClient::isSelectTrailingWhitespaceEnabled()
326 {
327     notImplemented();
328     return false;
329 }
330
331 void EditorClient::toggleContinuousSpellChecking()
332 {
333     WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView);
334
335     gboolean enabled;
336     g_object_get(settings, "enable-spell-checking", &enabled, NULL);
337
338     g_object_set(settings, "enable-spell-checking", !enabled, NULL);
339 }
340
341 void EditorClient::toggleGrammarChecking()
342 {
343 }
344
345 static const unsigned CtrlKey = 1 << 0;
346 static const unsigned AltKey = 1 << 1;
347 static const unsigned ShiftKey = 1 << 2;
348
349 struct KeyDownEntry {
350     unsigned virtualKey;
351     unsigned modifiers;
352     const char* name;
353 };
354
355 struct KeyPressEntry {
356     unsigned charCode;
357     unsigned modifiers;
358     const char* name;
359 };
360
361 static const KeyDownEntry keyDownEntries[] = {
362     { VK_LEFT,   0,                  "MoveLeft"                                    },
363     { VK_LEFT,   ShiftKey,           "MoveLeftAndModifySelection"                  },
364     { VK_LEFT,   CtrlKey,            "MoveWordLeft"                                },
365     { VK_LEFT,   CtrlKey | ShiftKey, "MoveWordLeftAndModifySelection"              },
366     { VK_RIGHT,  0,                  "MoveRight"                                   },
367     { VK_RIGHT,  ShiftKey,           "MoveRightAndModifySelection"                 },
368     { VK_RIGHT,  CtrlKey,            "MoveWordRight"                               },
369     { VK_RIGHT,  CtrlKey | ShiftKey, "MoveWordRightAndModifySelection"             },
370     { VK_UP,     0,                  "MoveUp"                                      },
371     { VK_UP,     ShiftKey,           "MoveUpAndModifySelection"                    },
372     { VK_PRIOR,  ShiftKey,           "MovePageUpAndModifySelection"                },
373     { VK_DOWN,   0,                  "MoveDown"                                    },
374     { VK_DOWN,   ShiftKey,           "MoveDownAndModifySelection"                  },
375     { VK_NEXT,   ShiftKey,           "MovePageDownAndModifySelection"              },
376     { VK_PRIOR,  0,                  "MovePageUp"                                  },
377     { VK_NEXT,   0,                  "MovePageDown"                                },
378     { VK_HOME,   0,                  "MoveToBeginningOfLine"                       },
379     { VK_HOME,   ShiftKey,           "MoveToBeginningOfLineAndModifySelection"     },
380     { VK_HOME,   CtrlKey,            "MoveToBeginningOfDocument"                   },
381     { VK_HOME,   CtrlKey | ShiftKey, "MoveToBeginningOfDocumentAndModifySelection" },
382
383     { VK_END,    0,                  "MoveToEndOfLine"                             },
384     { VK_END,    ShiftKey,           "MoveToEndOfLineAndModifySelection"           },
385     { VK_END,    CtrlKey,            "MoveToEndOfDocument"                         },
386     { VK_END,    CtrlKey | ShiftKey, "MoveToEndOfDocumentAndModifySelection"       },
387
388     { VK_BACK,   0,                  "DeleteBackward"                              },
389     { VK_BACK,   ShiftKey,           "DeleteBackward"                              },
390     { VK_DELETE, 0,                  "DeleteForward"                               },
391     { VK_BACK,   CtrlKey,            "DeleteWordBackward"                          },
392     { VK_DELETE, CtrlKey,            "DeleteWordForward"                           },
393
394     { 'B',       CtrlKey,            "ToggleBold"                                  },
395     { 'I',       CtrlKey,            "ToggleItalic"                                },
396
397     { VK_ESCAPE, 0,                  "Cancel"                                      },
398     { VK_OEM_PERIOD, CtrlKey,        "Cancel"                                      },
399     { VK_TAB,    0,                  "InsertTab"                                   },
400     { VK_TAB,    ShiftKey,           "InsertBacktab"                               },
401     { VK_RETURN, 0,                  "InsertNewline"                               },
402     { VK_RETURN, CtrlKey,            "InsertNewline"                               },
403     { VK_RETURN, AltKey,             "InsertNewline"                               },
404     { VK_RETURN, AltKey | ShiftKey,  "InsertNewline"                               },
405 };
406
407 static const KeyPressEntry keyPressEntries[] = {
408     { '\t',   0,                  "InsertTab"                                   },
409     { '\t',   ShiftKey,           "InsertBacktab"                               },
410     { '\r',   0,                  "InsertNewline"                               },
411     { '\r',   CtrlKey,            "InsertNewline"                               },
412     { '\r',   AltKey,             "InsertNewline"                               },
413     { '\r',   AltKey | ShiftKey,  "InsertNewline"                               },
414 };
415
416 static const char* interpretEditorCommandKeyEvent(const KeyboardEvent* evt)
417 {
418     ASSERT(evt->type() == eventNames().keydownEvent || evt->type() == eventNames().keypressEvent);
419
420     static HashMap<int, const char*>* keyDownCommandsMap = 0;
421     static HashMap<int, const char*>* keyPressCommandsMap = 0;
422
423     if (!keyDownCommandsMap) {
424         keyDownCommandsMap = new HashMap<int, const char*>;
425         keyPressCommandsMap = new HashMap<int, const char*>;
426
427         for (unsigned i = 0; i < G_N_ELEMENTS(keyDownEntries); i++)
428             keyDownCommandsMap->set(keyDownEntries[i].modifiers << 16 | keyDownEntries[i].virtualKey, keyDownEntries[i].name);
429
430         for (unsigned i = 0; i < G_N_ELEMENTS(keyPressEntries); i++)
431             keyPressCommandsMap->set(keyPressEntries[i].modifiers << 16 | keyPressEntries[i].charCode, keyPressEntries[i].name);
432     }
433
434     unsigned modifiers = 0;
435     if (evt->shiftKey())
436         modifiers |= ShiftKey;
437     if (evt->altKey())
438         modifiers |= AltKey;
439     if (evt->ctrlKey())
440         modifiers |= CtrlKey;
441
442     if (evt->type() == eventNames().keydownEvent) {
443         int mapKey = modifiers << 16 | evt->keyCode();
444         return mapKey ? keyDownCommandsMap->get(mapKey) : 0;
445     }
446
447     int mapKey = modifiers << 16 | evt->charCode();
448     return mapKey ? keyPressCommandsMap->get(mapKey) : 0;
449 }
450
451 void EditorClient::handleKeyboardEvent(KeyboardEvent* event)
452 {
453     Node* node = event->target()->toNode();
454     ASSERT(node);
455     Frame* frame = node->document()->frame();
456     ASSERT(frame);
457
458     const PlatformKeyboardEvent* platformEvent = event->keyEvent();
459     if (!platformEvent)
460         return;
461
462     // Don't allow editor commands or text insertion for nodes that
463     // cannot edit, unless we are in caret mode.
464     if (!frame->editor()->canEdit() && !(frame->settings() && frame->settings()->caretBrowsingEnabled()))
465         return;
466
467     const gchar* editorCommandString = interpretEditorCommandKeyEvent(event);
468     if (editorCommandString) {
469         Editor::Command command = frame->editor()->command(editorCommandString);
470
471         // On editor commands from key down events, we only want to let the event bubble up to
472         // the DOM if it inserts text. If it doesn't insert text (e.g. Tab that changes focus)
473         // we just want WebKit to handle it immediately without a DOM event.
474         if (platformEvent->type() == PlatformKeyboardEvent::RawKeyDown) {
475             if (!command.isTextInsertion() && command.execute(event))
476                 event->setDefaultHandled();
477
478             clearPendingComposition();
479             return;
480         }
481
482         if (command.execute(event)) {
483             clearPendingComposition();
484             event->setDefaultHandled();
485             return;
486         }
487     }
488
489     // This is just a normal text insertion, so wait to execute the insertion
490     // until a keypress event happens. This will ensure that the insertion will not
491     // be reflected in the contents of the field until the keyup DOM event.
492     if (event->type() == eventNames().keypressEvent) {
493
494         // If we have a pending composition at this point, it happened while
495         // filtering a keypress, so we treat it as a normal text insertion.
496         // This will also ensure that if the keypress event handler changed the
497         // currently focused node, the text is still inserted into the original
498         // node (insertText() has this logic, but confirmComposition() does not).
499         if (m_pendingComposition) {
500             frame->editor()->insertText(String::fromUTF8(m_pendingComposition.get()), event);
501             clearPendingComposition();
502             event->setDefaultHandled();
503
504         } else {
505             // Don't insert null or control characters as they can result in unexpected behaviour
506             if (event->charCode() < ' ')
507                 return;
508
509             // Don't insert anything if a modifier is pressed
510             if (platformEvent->ctrlKey() || platformEvent->altKey())
511                 return;
512
513             if (frame->editor()->insertText(platformEvent->text(), event))
514                 event->setDefaultHandled();
515         }
516     }
517 }
518
519 void EditorClient::handleInputMethodKeydown(KeyboardEvent* event)
520 {
521     Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame();
522     if (!targetFrame || !targetFrame->editor()->canEdit())
523         return;
524
525     WebKitWebViewPrivate* priv = m_webView->priv;
526
527
528     // Some IM contexts (e.g. 'simple') will act as if they filter every
529     // keystroke and just issue a 'commit' signal during handling. In situations
530     // where the 'commit' signal happens during filtering and there is no active
531     // composition, act as if the keystroke was not filtered. The one exception to
532     // this is when the keyval parameter of the GdkKeyEvent is 0, which is often
533     // a key event sent by the IM context for committing the current composition.
534
535     // Here is a typical sequence of events for the 'simple' context:
536     // 1. GDK key press event -> webkit_web_view_key_press_event
537     // 2. Keydown event -> EditorClient::handleInputMethodKeydown
538     //     gtk_im_context_filter_keypress returns true, but there is a pending
539     //     composition so event->preventDefault is not called (below).
540     // 3. Keydown event bubbles through the DOM
541     // 4. Keydown event -> EditorClient::handleKeyboardEvent
542     //     No action taken.
543     // 4. GDK key release event -> webkit_web_view_key_release_event
544     // 5. gtk_im_context_filter_keypress is called on the release event.
545     //     Simple does not filter most key releases, so the event continues.
546     // 6. Keypress event bubbles through the DOM.
547     // 7. Keypress event -> EditorClient::handleKeyboardEvent
548     //     pending composition is inserted.
549     // 8. Keyup event bubbles through the DOM.
550     // 9. Keyup event -> EditorClient::handleKeyboardEvent
551     //     No action taken.
552
553     // There are two situations where we do filter the keystroke:
554     // 1. The IMContext instructed us to filter and we have no pending composition.
555     // 2. The IMContext did not instruct us to filter, but the keystroke caused a
556     //    composition in progress to finish. It seems that sometimes SCIM will finish
557     //    a composition and not mark the keystroke as filtered.
558     m_treatContextCommitAsKeyEvent = (!targetFrame->editor()->hasComposition())
559          && event->keyEvent()->gdkEventKey()->keyval;
560     clearPendingComposition();
561     if ((gtk_im_context_filter_keypress(priv->imContext, event->keyEvent()->gdkEventKey()) && !m_pendingComposition)
562         || (!m_treatContextCommitAsKeyEvent && !targetFrame->editor()->hasComposition()))
563         event->preventDefault();
564
565     m_treatContextCommitAsKeyEvent = false;
566 }
567
568 EditorClient::EditorClient(WebKitWebView* webView)
569     : m_isInRedo(false)
570     , m_webView(webView)
571     , m_treatContextCommitAsKeyEvent(false)
572 {
573     WebKitWebViewPrivate* priv = m_webView->priv;
574     g_signal_connect(priv->imContext, "commit", G_CALLBACK(imContextCommitted), this);
575     g_signal_connect(priv->imContext, "preedit-changed", G_CALLBACK(imContextPreeditChanged), this);
576 }
577
578 EditorClient::~EditorClient()
579 {
580     WebKitWebViewPrivate* priv = m_webView->priv;
581     g_signal_handlers_disconnect_by_func(priv->imContext, (gpointer)imContextCommitted, this);
582     g_signal_handlers_disconnect_by_func(priv->imContext, (gpointer)imContextPreeditChanged, this);
583 }
584
585 void EditorClient::textFieldDidBeginEditing(Element*)
586 {
587 }
588
589 void EditorClient::textFieldDidEndEditing(Element*)
590 {
591 }
592
593 void EditorClient::textDidChangeInTextField(Element*)
594 {
595 }
596
597 bool EditorClient::doTextFieldCommandFromEvent(Element*, KeyboardEvent*)
598 {
599     return false;
600 }
601
602 void EditorClient::textWillBeDeletedInTextField(Element*)
603 {
604     notImplemented();
605 }
606
607 void EditorClient::textDidChangeInTextArea(Element*)
608 {
609     notImplemented();
610 }
611
612 void EditorClient::ignoreWordInSpellDocument(const String& text)
613 {
614     GSList* dicts = webkit_web_settings_get_enchant_dicts(m_webView);
615
616     for (; dicts; dicts = dicts->next) {
617         EnchantDict* dict = static_cast<EnchantDict*>(dicts->data);
618
619         enchant_dict_add_to_session(dict, text.utf8().data(), -1);
620     }
621 }
622
623 void EditorClient::learnWord(const String& text)
624 {
625     GSList* dicts = webkit_web_settings_get_enchant_dicts(m_webView);
626
627     for (; dicts; dicts = dicts->next) {
628         EnchantDict* dict = static_cast<EnchantDict*>(dicts->data);
629
630         enchant_dict_add_to_personal(dict, text.utf8().data(), -1);
631     }
632 }
633
634 void EditorClient::checkSpellingOfString(const UChar* text, int length, int* misspellingLocation, int* misspellingLength)
635 {
636     GSList* dicts = webkit_web_settings_get_enchant_dicts(m_webView);
637     if (!dicts)
638         return;
639
640     gchar* ctext = g_utf16_to_utf8(const_cast<gunichar2*>(text), length, 0, 0, 0);
641     int utflen = g_utf8_strlen(ctext, -1);
642
643     PangoLanguage* language = pango_language_get_default();
644     PangoLogAttr* attrs = g_new(PangoLogAttr, utflen+1);
645
646     // pango_get_log_attrs uses an aditional position at the end of the text.
647     pango_get_log_attrs(ctext, -1, -1, language, attrs, utflen+1);
648
649     for (int i = 0; i < length+1; i++) {
650         // We go through each character until we find an is_word_start,
651         // then we get into an inner loop to find the is_word_end corresponding
652         // to it.
653         if (attrs[i].is_word_start) {
654             int start = i;
655             int end = i;
656             int wordLength;
657
658             while (attrs[end].is_word_end < 1)
659                 end++;
660
661             wordLength = end - start;
662             // Set the iterator to be at the current word end, so we don't
663             // check characters twice.
664             i = end;
665
666             for (; dicts; dicts = dicts->next) {
667                 EnchantDict* dict = static_cast<EnchantDict*>(dicts->data);
668                 gchar* cstart = g_utf8_offset_to_pointer(ctext, start);
669                 gint bytes = static_cast<gint>(g_utf8_offset_to_pointer(ctext, end) - cstart);
670                 gchar* word = g_new0(gchar, bytes+1);
671                 int result;
672
673                 g_utf8_strncpy(word, cstart, end - start);
674
675                 result = enchant_dict_check(dict, word, -1);
676                 g_free(word);
677                 if (result) {
678                     *misspellingLocation = start;
679                     *misspellingLength = wordLength;
680                 } else {
681                     // Stop checking, this word is ok in at least one dict.
682                     *misspellingLocation = -1;
683                     *misspellingLength = 0;
684                     break;
685                 }
686             }
687         }
688     }
689
690     g_free(attrs);
691     g_free(ctext);
692 }
693
694 String EditorClient::getAutoCorrectSuggestionForMisspelledWord(const String& inputWord)
695 {
696     // This method can be implemented using customized algorithms for the particular browser.
697     // Currently, it computes an empty string.
698     return String();
699 }
700
701 void EditorClient::checkGrammarOfString(const UChar*, int, Vector<GrammarDetail>&, int*, int*)
702 {
703     notImplemented();
704 }
705
706 void EditorClient::updateSpellingUIWithGrammarString(const String&, const GrammarDetail&)
707 {
708     notImplemented();
709 }
710
711 void EditorClient::updateSpellingUIWithMisspelledWord(const String&)
712 {
713     notImplemented();
714 }
715
716 void EditorClient::showSpellingUI(bool)
717 {
718     notImplemented();
719 }
720
721 bool EditorClient::spellingUIIsShowing()
722 {
723     notImplemented();
724     return false;
725 }
726
727 void EditorClient::getGuessesForWord(const String& word, WTF::Vector<String>& guesses)
728 {
729     GSList* dicts = webkit_web_settings_get_enchant_dicts(m_webView);
730     guesses.clear();
731
732     for (; dicts; dicts = dicts->next) {
733         size_t numberOfSuggestions;
734         size_t i;
735
736         EnchantDict* dict = static_cast<EnchantDict*>(dicts->data);
737         gchar** suggestions = enchant_dict_suggest(dict, word.utf8().data(), -1, &numberOfSuggestions);
738
739         for (i = 0; i < numberOfSuggestions && i < 10; i++)
740             guesses.append(String::fromUTF8(suggestions[i]));
741
742         if (numberOfSuggestions > 0)
743             enchant_dict_free_suggestions(dict, suggestions);
744     }
745 }
746
747 }