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, 2010 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 "DumpRenderTreeSupportGtk.h"
29 #include "EditCommand.h"
30 #include "Editor.h"
31 #include "EventNames.h"
32 #include "FocusController.h"
33 #include "Frame.h"
34 #include <glib.h>
35 #include "KeyboardEvent.h"
36 #include "markup.h"
37 #include "NotImplemented.h"
38 #include "Page.h"
39 #include "PasteboardHelperGtk.h"
40 #include "PlatformKeyboardEvent.h"
41 #include "WebKitDOMBinding.h"
42 #include "WebKitDOMCSSStyleDeclarationPrivate.h"
43 #include "WebKitDOMHTMLElementPrivate.h"
44 #include "WebKitDOMNodePrivate.h"
45 #include "WebKitDOMRangePrivate.h"
46 #include "WindowsKeyboardCodes.h"
47 #include "webkitglobalsprivate.h"
48 #include "webkitmarshal.h"
49 #include "webkitwebsettingsprivate.h"
50 #include "webkitwebviewprivate.h"
51 #include <wtf/text/CString.h>
52
53 // Arbitrary depth limit for the undo stack, to keep it from using
54 // unbounded memory. This is the maximum number of distinct undoable
55 // actions -- unbroken stretches of typed characters are coalesced
56 // into a single action.
57 #define maximumUndoStackDepth 1000
58
59 using namespace WebCore;
60
61 namespace WebKit {
62
imContextCommitted(GtkIMContext * context,const gchar * compositionString,EditorClient * client)63 static void imContextCommitted(GtkIMContext* context, const gchar* compositionString, EditorClient* client)
64 {
65 Frame* frame = core(client->webView())->focusController()->focusedOrMainFrame();
66 if (!frame || !frame->editor()->canEdit())
67 return;
68
69 // If this signal fires during a keydown event when we are not in the middle
70 // of a composition, then treat this 'commit' as a normal key event and just
71 // change the editable area right before the keypress event.
72 if (client->treatContextCommitAsKeyEvent()) {
73 client->updatePendingComposition(compositionString);
74 return;
75 }
76
77 // If this signal fires during a mousepress event when we are in the middle
78 // of a composition, skip this 'commit' because the composition is already confirmed.
79 if (client->preventNextCompositionCommit())
80 return;
81
82 frame->editor()->confirmComposition(String::fromUTF8(compositionString));
83 client->clearPendingComposition();
84 }
85
imContextPreeditChanged(GtkIMContext * context,EditorClient * client)86 static void imContextPreeditChanged(GtkIMContext* context, EditorClient* client)
87 {
88 Frame* frame = core(client->webView())->focusController()->focusedOrMainFrame();
89 if (!frame || !frame->editor()->canEdit())
90 return;
91
92 // We ignore the provided PangoAttrList for now.
93 GOwnPtr<gchar> newPreedit(0);
94 gtk_im_context_get_preedit_string(context, &newPreedit.outPtr(), 0, 0);
95
96 String preeditString = String::fromUTF8(newPreedit.get());
97 Vector<CompositionUnderline> underlines;
98 underlines.append(CompositionUnderline(0, preeditString.length(), Color(0, 0, 0), false));
99 frame->editor()->setComposition(preeditString, underlines, 0, 0);
100 }
101
backspaceCallback(GtkWidget * widget,EditorClient * client)102 static void backspaceCallback(GtkWidget* widget, EditorClient* client)
103 {
104 g_signal_stop_emission_by_name(widget, "backspace");
105 client->addPendingEditorCommand("DeleteBackward");
106 }
107
selectAllCallback(GtkWidget * widget,gboolean select,EditorClient * client)108 static void selectAllCallback(GtkWidget* widget, gboolean select, EditorClient* client)
109 {
110 g_signal_stop_emission_by_name(widget, "select-all");
111 client->addPendingEditorCommand(select ? "SelectAll" : "Unselect");
112 }
113
cutClipboardCallback(GtkWidget * widget,EditorClient * client)114 static void cutClipboardCallback(GtkWidget* widget, EditorClient* client)
115 {
116 g_signal_stop_emission_by_name(widget, "cut-clipboard");
117 client->addPendingEditorCommand("Cut");
118 }
119
copyClipboardCallback(GtkWidget * widget,EditorClient * client)120 static void copyClipboardCallback(GtkWidget* widget, EditorClient* client)
121 {
122 g_signal_stop_emission_by_name(widget, "copy-clipboard");
123 client->addPendingEditorCommand("Copy");
124 }
125
pasteClipboardCallback(GtkWidget * widget,EditorClient * client)126 static void pasteClipboardCallback(GtkWidget* widget, EditorClient* client)
127 {
128 g_signal_stop_emission_by_name(widget, "paste-clipboard");
129 client->addPendingEditorCommand("Paste");
130 }
131
toggleOverwriteCallback(GtkWidget * widget,EditorClient *)132 static void toggleOverwriteCallback(GtkWidget* widget, EditorClient*)
133 {
134 // We don't support toggling the overwrite mode, but the default callback expects
135 // the GtkTextView to have a layout, so we handle this signal just to stop it.
136 g_signal_stop_emission_by_name(widget, "toggle-overwrite");
137 }
138
139 // GTK+ will still send these signals to the web view. So we can safely stop signal
140 // emission without breaking accessibility.
popupMenuCallback(GtkWidget * widget,EditorClient *)141 static void popupMenuCallback(GtkWidget* widget, EditorClient*)
142 {
143 g_signal_stop_emission_by_name(widget, "popup-menu");
144 }
145
showHelpCallback(GtkWidget * widget,EditorClient *)146 static void showHelpCallback(GtkWidget* widget, EditorClient*)
147 {
148 g_signal_stop_emission_by_name(widget, "show-help");
149 }
150
151 static const char* const gtkDeleteCommands[][2] = {
152 { "DeleteBackward", "DeleteForward" }, // Characters
153 { "DeleteWordBackward", "DeleteWordForward" }, // Word ends
154 { "DeleteWordBackward", "DeleteWordForward" }, // Words
155 { "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Lines
156 { "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Line ends
157 { "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraph ends
158 { "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraphs
159 { 0, 0 } // Whitespace (M-\ in Emacs)
160 };
161
deleteFromCursorCallback(GtkWidget * widget,GtkDeleteType deleteType,gint count,EditorClient * client)162 static void deleteFromCursorCallback(GtkWidget* widget, GtkDeleteType deleteType, gint count, EditorClient* client)
163 {
164 g_signal_stop_emission_by_name(widget, "delete-from-cursor");
165 int direction = count > 0 ? 1 : 0;
166
167 // Ensuring that deleteType <= G_N_ELEMENTS here results in a compiler warning
168 // that the condition is always true.
169
170 if (deleteType == GTK_DELETE_WORDS) {
171 if (!direction) {
172 client->addPendingEditorCommand("MoveWordForward");
173 client->addPendingEditorCommand("MoveWordBackward");
174 } else {
175 client->addPendingEditorCommand("MoveWordBackward");
176 client->addPendingEditorCommand("MoveWordForward");
177 }
178 } else if (deleteType == GTK_DELETE_DISPLAY_LINES) {
179 if (!direction)
180 client->addPendingEditorCommand("MoveToBeginningOfLine");
181 else
182 client->addPendingEditorCommand("MoveToEndOfLine");
183 } else if (deleteType == GTK_DELETE_PARAGRAPHS) {
184 if (!direction)
185 client->addPendingEditorCommand("MoveToBeginningOfParagraph");
186 else
187 client->addPendingEditorCommand("MoveToEndOfParagraph");
188 }
189
190 const char* rawCommand = gtkDeleteCommands[deleteType][direction];
191 if (!rawCommand)
192 return;
193
194 for (int i = 0; i < abs(count); i++)
195 client->addPendingEditorCommand(rawCommand);
196 }
197
198 static const char* const gtkMoveCommands[][4] = {
199 { "MoveBackward", "MoveForward",
200 "MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Forward/backward grapheme
201 { "MoveLeft", "MoveRight",
202 "MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Left/right grapheme
203 { "MoveWordBackward", "MoveWordForward",
204 "MoveWordBackwardAndModifySelection", "MoveWordForwardAndModifySelection" }, // Forward/backward word
205 { "MoveUp", "MoveDown",
206 "MoveUpAndModifySelection", "MoveDownAndModifySelection" }, // Up/down line
207 { "MoveToBeginningOfLine", "MoveToEndOfLine",
208 "MoveToBeginningOfLineAndModifySelection", "MoveToEndOfLineAndModifySelection" }, // Up/down line ends
209 { "MoveParagraphForward", "MoveParagraphBackward",
210 "MoveParagraphForwardAndModifySelection", "MoveParagraphBackwardAndModifySelection" }, // Up/down paragraphs
211 { "MoveToBeginningOfParagraph", "MoveToEndOfParagraph",
212 "MoveToBeginningOfParagraphAndModifySelection", "MoveToEndOfParagraphAndModifySelection" }, // Up/down paragraph ends.
213 { "MovePageUp", "MovePageDown",
214 "MovePageUpAndModifySelection", "MovePageDownAndModifySelection" }, // Up/down page
215 { "MoveToBeginningOfDocument", "MoveToEndOfDocument",
216 "MoveToBeginningOfDocumentAndModifySelection", "MoveToEndOfDocumentAndModifySelection" }, // Begin/end of buffer
217 { 0, 0,
218 0, 0 } // Horizontal page movement
219 };
220
moveCursorCallback(GtkWidget * widget,GtkMovementStep step,gint count,gboolean extendSelection,EditorClient * client)221 static void moveCursorCallback(GtkWidget* widget, GtkMovementStep step, gint count, gboolean extendSelection, EditorClient* client)
222 {
223 g_signal_stop_emission_by_name(widget, "move-cursor");
224 int direction = count > 0 ? 1 : 0;
225 if (extendSelection)
226 direction += 2;
227
228 if (static_cast<unsigned>(step) >= G_N_ELEMENTS(gtkMoveCommands))
229 return;
230
231 const char* rawCommand = gtkMoveCommands[step][direction];
232 if (!rawCommand)
233 return;
234
235 for (int i = 0; i < abs(count); i++)
236 client->addPendingEditorCommand(rawCommand);
237 }
238
updatePendingComposition(const gchar * newComposition)239 void EditorClient::updatePendingComposition(const gchar* newComposition)
240 {
241 // The IMContext may signal more than one completed composition in a row,
242 // in which case we want to append them, rather than overwrite the old one.
243 if (!m_pendingComposition)
244 m_pendingComposition.set(g_strdup(newComposition));
245 else
246 m_pendingComposition.set(g_strconcat(m_pendingComposition.get(), newComposition, NULL));
247 }
248
willSetInputMethodState()249 void EditorClient::willSetInputMethodState()
250 {
251 }
252
setInputMethodState(bool active)253 void EditorClient::setInputMethodState(bool active)
254 {
255 WebKitWebViewPrivate* priv = m_webView->priv;
256
257 if (active)
258 gtk_im_context_focus_in(priv->imContext.get());
259 else
260 gtk_im_context_focus_out(priv->imContext.get());
261
262 #ifdef MAEMO_CHANGES
263 if (active)
264 hildon_gtk_im_context_show(priv->imContext.get());
265 else
266 hildon_gtk_im_context_hide(priv->imContext.get());
267 #endif
268 }
269
shouldDeleteRange(Range * range)270 bool EditorClient::shouldDeleteRange(Range* range)
271 {
272 gboolean accept = TRUE;
273 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
274 g_signal_emit_by_name(m_webView, "should-delete-range", kitRange.get(), &accept);
275 return accept;
276 }
277
shouldShowDeleteInterface(HTMLElement * element)278 bool EditorClient::shouldShowDeleteInterface(HTMLElement* element)
279 {
280 gboolean accept = TRUE;
281 GRefPtr<WebKitDOMHTMLElement> kitElement(adoptGRef(kit(element)));
282 g_signal_emit_by_name(m_webView, "should-show-delete-interface-for-element", kitElement.get(), &accept);
283 return accept;
284 }
285
isContinuousSpellCheckingEnabled()286 bool EditorClient::isContinuousSpellCheckingEnabled()
287 {
288 WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView);
289
290 gboolean enabled;
291 g_object_get(settings, "enable-spell-checking", &enabled, NULL);
292
293 return enabled;
294 }
295
isGrammarCheckingEnabled()296 bool EditorClient::isGrammarCheckingEnabled()
297 {
298 notImplemented();
299 return false;
300 }
301
spellCheckerDocumentTag()302 int EditorClient::spellCheckerDocumentTag()
303 {
304 notImplemented();
305 return 0;
306 }
307
shouldBeginEditing(WebCore::Range * range)308 bool EditorClient::shouldBeginEditing(WebCore::Range* range)
309 {
310 clearPendingComposition();
311
312 gboolean accept = TRUE;
313 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
314 g_signal_emit_by_name(m_webView, "should-begin-editing", kitRange.get(), &accept);
315 return accept;
316 }
317
shouldEndEditing(WebCore::Range * range)318 bool EditorClient::shouldEndEditing(WebCore::Range* range)
319 {
320 clearPendingComposition();
321
322 gboolean accept = TRUE;
323 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
324 g_signal_emit_by_name(m_webView, "should-end-editing", kitRange.get(), &accept);
325 return accept;
326 }
327
kit(EditorInsertAction action)328 static WebKitInsertAction kit(EditorInsertAction action)
329 {
330 switch (action) {
331 case EditorInsertActionTyped:
332 return WEBKIT_INSERT_ACTION_TYPED;
333 case EditorInsertActionPasted:
334 return WEBKIT_INSERT_ACTION_PASTED;
335 case EditorInsertActionDropped:
336 return WEBKIT_INSERT_ACTION_DROPPED;
337 }
338 ASSERT_NOT_REACHED();
339 return WEBKIT_INSERT_ACTION_TYPED;
340 }
341
shouldInsertText(const String & string,Range * range,EditorInsertAction action)342 bool EditorClient::shouldInsertText(const String& string, Range* range, EditorInsertAction action)
343 {
344 gboolean accept = TRUE;
345 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
346 g_signal_emit_by_name(m_webView, "should-insert-text", string.utf8().data(), kitRange.get(), kit(action), &accept);
347 return accept;
348 }
349
kit(EAffinity affinity)350 static WebKitSelectionAffinity kit(EAffinity affinity)
351 {
352 switch (affinity) {
353 case UPSTREAM:
354 return WEBKIT_SELECTION_AFFINITY_UPSTREAM;
355 case DOWNSTREAM:
356 return WEBKIT_SELECTION_AFFINITY_DOWNSTREAM;
357 }
358 ASSERT_NOT_REACHED();
359 return WEBKIT_SELECTION_AFFINITY_UPSTREAM;
360 }
361
shouldChangeSelectedRange(Range * fromRange,Range * toRange,EAffinity affinity,bool stillSelecting)362 bool EditorClient::shouldChangeSelectedRange(Range* fromRange, Range* toRange, EAffinity affinity, bool stillSelecting)
363 {
364 gboolean accept = TRUE;
365 GRefPtr<WebKitDOMRange> kitFromRange(fromRange ? adoptGRef(kit(fromRange)) : 0);
366 GRefPtr<WebKitDOMRange> kitToRange(toRange ? adoptGRef(kit(toRange)) : 0);
367 g_signal_emit_by_name(m_webView, "should-change-selected-range", kitFromRange.get(), kitToRange.get(),
368 kit(affinity), stillSelecting, &accept);
369 return accept;
370 }
371
shouldApplyStyle(WebCore::CSSStyleDeclaration * declaration,WebCore::Range * range)372 bool EditorClient::shouldApplyStyle(WebCore::CSSStyleDeclaration* declaration, WebCore::Range* range)
373 {
374 gboolean accept = TRUE;
375 GRefPtr<WebKitDOMCSSStyleDeclaration> kitDeclaration(kit(declaration));
376 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
377 g_signal_emit_by_name(m_webView, "should-apply-style", kitDeclaration.get(), kitRange.get(), &accept);
378 return accept;
379 }
380
shouldMoveRangeAfterDelete(WebCore::Range *,WebCore::Range *)381 bool EditorClient::shouldMoveRangeAfterDelete(WebCore::Range*, WebCore::Range*)
382 {
383 notImplemented();
384 return true;
385 }
386
didBeginEditing()387 void EditorClient::didBeginEditing()
388 {
389 g_signal_emit_by_name(m_webView, "editing-began");
390 }
391
respondToChangedContents()392 void EditorClient::respondToChangedContents()
393 {
394 g_signal_emit_by_name(m_webView, "user-changed-contents");
395 }
396
397 static WebKitWebView* viewSettingClipboard = 0;
collapseSelection(GtkClipboard * clipboard,WebKitWebView * webView)398 static void collapseSelection(GtkClipboard* clipboard, WebKitWebView* webView)
399 {
400 if (viewSettingClipboard && viewSettingClipboard == webView)
401 return;
402
403 WebCore::Page* corePage = core(webView);
404 if (!corePage || !corePage->focusController())
405 return;
406
407 Frame* frame = corePage->focusController()->focusedOrMainFrame();
408
409 // Collapse the selection without clearing it
410 ASSERT(frame);
411 frame->selection()->setBase(frame->selection()->extent(), frame->selection()->affinity());
412 }
413
414 #if PLATFORM(X11)
setSelectionPrimaryClipboardIfNeeded(WebKitWebView * webView)415 static void setSelectionPrimaryClipboardIfNeeded(WebKitWebView* webView)
416 {
417 if (!gtk_widget_has_screen(GTK_WIDGET(webView)))
418 return;
419
420 GtkClipboard* clipboard = gtk_widget_get_clipboard(GTK_WIDGET(webView), GDK_SELECTION_PRIMARY);
421 DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
422 WebCore::Page* corePage = core(webView);
423 Frame* targetFrame = corePage->focusController()->focusedOrMainFrame();
424
425 if (!targetFrame->selection()->isRange())
426 return;
427
428 dataObject->clear();
429 dataObject->setRange(targetFrame->selection()->toNormalizedRange());
430
431 viewSettingClipboard = webView;
432 GClosure* callback = g_cclosure_new_object(G_CALLBACK(collapseSelection), G_OBJECT(webView));
433 g_closure_set_marshal(callback, g_cclosure_marshal_VOID__VOID);
434 pasteboardHelperInstance()->writeClipboardContents(clipboard, callback);
435 viewSettingClipboard = 0;
436 }
437 #endif
438
respondToChangedSelection()439 void EditorClient::respondToChangedSelection()
440 {
441 g_signal_emit_by_name(m_webView, "selection-changed");
442
443 WebKitWebViewPrivate* priv = m_webView->priv;
444 WebCore::Page* corePage = core(m_webView);
445 Frame* targetFrame = corePage->focusController()->focusedOrMainFrame();
446
447 if (!targetFrame)
448 return;
449
450 if (targetFrame->editor()->ignoreCompositionSelectionChange())
451 return;
452
453 #if PLATFORM(X11)
454 setSelectionPrimaryClipboardIfNeeded(m_webView);
455 #endif
456
457 if (!targetFrame->editor()->hasComposition())
458 return;
459
460 unsigned start;
461 unsigned end;
462 if (!targetFrame->editor()->getCompositionSelection(start, end)) {
463 // gtk_im_context_reset() clears the composition for us.
464 gtk_im_context_reset(priv->imContext.get());
465 targetFrame->editor()->confirmCompositionWithoutDisturbingSelection();
466 }
467 }
468
didEndEditing()469 void EditorClient::didEndEditing()
470 {
471 g_signal_emit_by_name(m_webView, "editing-ended");
472 }
473
didWriteSelectionToPasteboard()474 void EditorClient::didWriteSelectionToPasteboard()
475 {
476 notImplemented();
477 }
478
didSetSelectionTypesForPasteboard()479 void EditorClient::didSetSelectionTypesForPasteboard()
480 {
481 notImplemented();
482 }
483
registerCommandForUndo(WTF::PassRefPtr<WebCore::EditCommand> command)484 void EditorClient::registerCommandForUndo(WTF::PassRefPtr<WebCore::EditCommand> command)
485 {
486 if (undoStack.size() == maximumUndoStackDepth)
487 undoStack.removeFirst();
488 if (!m_isInRedo)
489 redoStack.clear();
490 undoStack.append(command);
491 }
492
registerCommandForRedo(WTF::PassRefPtr<WebCore::EditCommand> command)493 void EditorClient::registerCommandForRedo(WTF::PassRefPtr<WebCore::EditCommand> command)
494 {
495 redoStack.append(command);
496 }
497
clearUndoRedoOperations()498 void EditorClient::clearUndoRedoOperations()
499 {
500 undoStack.clear();
501 redoStack.clear();
502 }
503
canCopyCut(bool defaultValue) const504 bool EditorClient::canCopyCut(bool defaultValue) const
505 {
506 return defaultValue;
507 }
508
canPaste(bool defaultValue) const509 bool EditorClient::canPaste(bool defaultValue) const
510 {
511 return defaultValue;
512 }
513
canUndo() const514 bool EditorClient::canUndo() const
515 {
516 return !undoStack.isEmpty();
517 }
518
canRedo() const519 bool EditorClient::canRedo() const
520 {
521 return !redoStack.isEmpty();
522 }
523
undo()524 void EditorClient::undo()
525 {
526 if (canUndo()) {
527 RefPtr<WebCore::EditCommand> command(*(--undoStack.end()));
528 undoStack.remove(--undoStack.end());
529 // unapply will call us back to push this command onto the redo stack.
530 command->unapply();
531 }
532 }
533
redo()534 void EditorClient::redo()
535 {
536 if (canRedo()) {
537 RefPtr<WebCore::EditCommand> command(*(--redoStack.end()));
538 redoStack.remove(--redoStack.end());
539
540 ASSERT(!m_isInRedo);
541 m_isInRedo = true;
542 // reapply will call us back to push this command onto the undo stack.
543 command->reapply();
544 m_isInRedo = false;
545 }
546 }
547
shouldInsertNode(Node * node,Range * range,EditorInsertAction action)548 bool EditorClient::shouldInsertNode(Node* node, Range* range, EditorInsertAction action)
549 {
550 gboolean accept = TRUE;
551 GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
552 GRefPtr<WebKitDOMNode> kitNode(adoptGRef(kit(node)));
553 g_signal_emit_by_name(m_webView, "should-insert-node", kitNode.get(), kitRange.get(), kit(action), &accept);
554 return accept;
555 }
556
pageDestroyed()557 void EditorClient::pageDestroyed()
558 {
559 delete this;
560 }
561
smartInsertDeleteEnabled()562 bool EditorClient::smartInsertDeleteEnabled()
563 {
564 notImplemented();
565 return false;
566 }
567
isSelectTrailingWhitespaceEnabled()568 bool EditorClient::isSelectTrailingWhitespaceEnabled()
569 {
570 if (!DumpRenderTreeSupportGtk::dumpRenderTreeModeEnabled())
571 return false;
572 return DumpRenderTreeSupportGtk::selectTrailingWhitespaceEnabled();
573 }
574
toggleContinuousSpellChecking()575 void EditorClient::toggleContinuousSpellChecking()
576 {
577 WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView);
578
579 gboolean enabled;
580 g_object_get(settings, "enable-spell-checking", &enabled, NULL);
581
582 g_object_set(settings, "enable-spell-checking", !enabled, NULL);
583 }
584
toggleGrammarChecking()585 void EditorClient::toggleGrammarChecking()
586 {
587 }
588
589 static const unsigned CtrlKey = 1 << 0;
590 static const unsigned AltKey = 1 << 1;
591 static const unsigned ShiftKey = 1 << 2;
592
593 struct KeyDownEntry {
594 unsigned virtualKey;
595 unsigned modifiers;
596 const char* name;
597 };
598
599 struct KeyPressEntry {
600 unsigned charCode;
601 unsigned modifiers;
602 const char* name;
603 };
604
605 static const KeyDownEntry keyDownEntries[] = {
606 { 'B', CtrlKey, "ToggleBold" },
607 { 'I', CtrlKey, "ToggleItalic" },
608 { VK_ESCAPE, 0, "Cancel" },
609 { VK_OEM_PERIOD, CtrlKey, "Cancel" },
610 { VK_TAB, 0, "InsertTab" },
611 { VK_TAB, ShiftKey, "InsertBacktab" },
612 { VK_RETURN, 0, "InsertNewline" },
613 { VK_RETURN, CtrlKey, "InsertNewline" },
614 { VK_RETURN, AltKey, "InsertNewline" },
615 { VK_RETURN, AltKey | ShiftKey, "InsertNewline" },
616 };
617
618 static const KeyPressEntry keyPressEntries[] = {
619 { '\t', 0, "InsertTab" },
620 { '\t', ShiftKey, "InsertBacktab" },
621 { '\r', 0, "InsertNewline" },
622 { '\r', CtrlKey, "InsertNewline" },
623 { '\r', AltKey, "InsertNewline" },
624 { '\r', AltKey | ShiftKey, "InsertNewline" },
625 };
626
generateEditorCommands(const KeyboardEvent * event)627 void EditorClient::generateEditorCommands(const KeyboardEvent* event)
628 {
629 ASSERT(event->type() == eventNames().keydownEvent || event->type() == eventNames().keypressEvent);
630
631 m_pendingEditorCommands.clear();
632
633 // First try to interpret the command as a native GTK+ key binding.
634 #ifdef GTK_API_VERSION_2
635 gtk_bindings_activate_event(GTK_OBJECT(m_nativeWidget.get()), event->keyEvent()->gdkEventKey());
636 #else
637 gtk_bindings_activate_event(G_OBJECT(m_nativeWidget.get()), event->keyEvent()->gdkEventKey());
638 #endif
639 if (m_pendingEditorCommands.size() > 0)
640 return;
641
642 static HashMap<int, const char*> keyDownCommandsMap;
643 static HashMap<int, const char*> keyPressCommandsMap;
644
645 if (keyDownCommandsMap.isEmpty()) {
646 for (unsigned i = 0; i < G_N_ELEMENTS(keyDownEntries); i++)
647 keyDownCommandsMap.set(keyDownEntries[i].modifiers << 16 | keyDownEntries[i].virtualKey, keyDownEntries[i].name);
648
649 for (unsigned i = 0; i < G_N_ELEMENTS(keyPressEntries); i++)
650 keyPressCommandsMap.set(keyPressEntries[i].modifiers << 16 | keyPressEntries[i].charCode, keyPressEntries[i].name);
651 }
652
653 unsigned modifiers = 0;
654 if (event->shiftKey())
655 modifiers |= ShiftKey;
656 if (event->altKey())
657 modifiers |= AltKey;
658 if (event->ctrlKey())
659 modifiers |= CtrlKey;
660
661 // For keypress events, we want charCode(), but keyCode() does that.
662 int mapKey = modifiers << 16 | event->keyCode();
663 if (!mapKey)
664 return;
665 HashMap<int, const char*>* commandMap = event->type() == eventNames().keydownEvent ?
666 &keyDownCommandsMap : &keyPressCommandsMap;
667 if (const char* commandString = commandMap->get(mapKey))
668 m_pendingEditorCommands.append(commandString);
669 }
670
executePendingEditorCommands(Frame * frame,bool allowTextInsertion)671 bool EditorClient::executePendingEditorCommands(Frame* frame, bool allowTextInsertion)
672 {
673 Vector<Editor::Command> commands;
674 for (size_t i = 0; i < m_pendingEditorCommands.size(); i++) {
675 const char* commandString = m_pendingEditorCommands.at(i);
676 ASSERT(commandString);
677 Editor::Command command = frame->editor()->command(commandString);
678 if (command.isTextInsertion() && !allowTextInsertion)
679 return false;
680
681 commands.append(command);
682 }
683
684 bool success = true;
685 for (size_t i = 0; i < commands.size(); i++) {
686 if (!commands.at(i).execute()) {
687 success = false;
688 break;
689 }
690 }
691
692 m_pendingEditorCommands.clear();
693
694 // If we successfully completed all editor commands, then
695 // this signals a canceling of the composition.
696 if (success)
697 clearPendingComposition();
698
699 return success;
700 }
701
handleKeyboardEvent(KeyboardEvent * event)702 void EditorClient::handleKeyboardEvent(KeyboardEvent* event)
703 {
704 Node* node = event->target()->toNode();
705 ASSERT(node);
706 Frame* frame = node->document()->frame();
707 ASSERT(frame);
708
709 const PlatformKeyboardEvent* platformEvent = event->keyEvent();
710 if (!platformEvent)
711 return;
712
713 generateEditorCommands(event);
714 if (m_pendingEditorCommands.size() > 0) {
715
716 // During RawKeyDown events if an editor command will insert text, defer
717 // the insertion until the keypress event. We want keydown to bubble up
718 // through the DOM first.
719 if (platformEvent->type() == PlatformKeyboardEvent::RawKeyDown) {
720 if (executePendingEditorCommands(frame, false))
721 event->setDefaultHandled();
722
723 return;
724 }
725
726 // Only allow text insertion commands if the current node is editable.
727 if (executePendingEditorCommands(frame, frame->editor()->canEdit())) {
728 event->setDefaultHandled();
729 return;
730 }
731 }
732
733 // Don't allow text insertion for nodes that cannot edit.
734 if (!frame->editor()->canEdit())
735 return;
736
737 // This is just a normal text insertion, so wait to execute the insertion
738 // until a keypress event happens. This will ensure that the insertion will not
739 // be reflected in the contents of the field until the keyup DOM event.
740 if (event->type() == eventNames().keypressEvent) {
741
742 // If we have a pending composition at this point, it happened while
743 // filtering a keypress, so we treat it as a normal text insertion.
744 // This will also ensure that if the keypress event handler changed the
745 // currently focused node, the text is still inserted into the original
746 // node (insertText() has this logic, but confirmComposition() does not).
747 if (m_pendingComposition) {
748 frame->editor()->insertText(String::fromUTF8(m_pendingComposition.get()), event);
749 clearPendingComposition();
750 event->setDefaultHandled();
751
752 } else {
753 // Don't insert null or control characters as they can result in unexpected behaviour
754 if (event->charCode() < ' ')
755 return;
756
757 // Don't insert anything if a modifier is pressed
758 if (platformEvent->ctrlKey() || platformEvent->altKey())
759 return;
760
761 if (frame->editor()->insertText(platformEvent->text(), event))
762 event->setDefaultHandled();
763 }
764 }
765 }
766
handleInputMethodKeydown(KeyboardEvent * event)767 void EditorClient::handleInputMethodKeydown(KeyboardEvent* event)
768 {
769 Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame();
770 if (!targetFrame || !targetFrame->editor()->canEdit())
771 return;
772
773 WebKitWebViewPrivate* priv = m_webView->priv;
774
775 m_preventNextCompositionCommit = false;
776
777 // Some IM contexts (e.g. 'simple') will act as if they filter every
778 // keystroke and just issue a 'commit' signal during handling. In situations
779 // where the 'commit' signal happens during filtering and there is no active
780 // composition, act as if the keystroke was not filtered. The one exception to
781 // this is when the keyval parameter of the GdkKeyEvent is 0, which is often
782 // a key event sent by the IM context for committing the current composition.
783
784 // Here is a typical sequence of events for the 'simple' context:
785 // 1. GDK key press event -> webkit_web_view_key_press_event
786 // 2. Keydown event -> EditorClient::handleInputMethodKeydown
787 // gtk_im_context_filter_keypress returns true, but there is a pending
788 // composition so event->preventDefault is not called (below).
789 // 3. Keydown event bubbles through the DOM
790 // 4. Keydown event -> EditorClient::handleKeyboardEvent
791 // No action taken.
792 // 4. GDK key release event -> webkit_web_view_key_release_event
793 // 5. gtk_im_context_filter_keypress is called on the release event.
794 // Simple does not filter most key releases, so the event continues.
795 // 6. Keypress event bubbles through the DOM.
796 // 7. Keypress event -> EditorClient::handleKeyboardEvent
797 // pending composition is inserted.
798 // 8. Keyup event bubbles through the DOM.
799 // 9. Keyup event -> EditorClient::handleKeyboardEvent
800 // No action taken.
801
802 // There are two situations where we do filter the keystroke:
803 // 1. The IMContext instructed us to filter and we have no pending composition.
804 // 2. The IMContext did not instruct us to filter, but the keystroke caused a
805 // composition in progress to finish. It seems that sometimes SCIM will finish
806 // a composition and not mark the keystroke as filtered.
807 m_treatContextCommitAsKeyEvent = (!targetFrame->editor()->hasComposition())
808 && event->keyEvent()->gdkEventKey()->keyval;
809 clearPendingComposition();
810 if ((gtk_im_context_filter_keypress(priv->imContext.get(), event->keyEvent()->gdkEventKey()) && !m_pendingComposition)
811 || (!m_treatContextCommitAsKeyEvent && !targetFrame->editor()->hasComposition()))
812 event->preventDefault();
813
814 m_treatContextCommitAsKeyEvent = false;
815 }
816
handleInputMethodMousePress()817 void EditorClient::handleInputMethodMousePress()
818 {
819 Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame();
820
821 if (!targetFrame || !targetFrame->editor()->canEdit())
822 return;
823
824 WebKitWebViewPrivate* priv = m_webView->priv;
825
826 // When a mouse press fires, the commit signal happens during a composition.
827 // In this case, if the focused node is changed, the commit signal happens in a diffrent node.
828 // Therefore, we need to confirm the current compositon and ignore the next commit signal.
829 GOwnPtr<gchar> newPreedit(0);
830 gtk_im_context_get_preedit_string(priv->imContext.get(), &newPreedit.outPtr(), 0, 0);
831
832 if (g_utf8_strlen(newPreedit.get(), -1)) {
833 targetFrame->editor()->confirmComposition();
834 m_preventNextCompositionCommit = true;
835 gtk_im_context_reset(priv->imContext.get());
836 }
837 }
838
EditorClient(WebKitWebView * webView)839 EditorClient::EditorClient(WebKitWebView* webView)
840 : m_isInRedo(false)
841 #if ENABLE(SPELLCHECK)
842 , m_textCheckerClient(webView)
843 #endif
844 , m_webView(webView)
845 , m_preventNextCompositionCommit(false)
846 , m_treatContextCommitAsKeyEvent(false)
847 , m_nativeWidget(gtk_text_view_new())
848 {
849 WebKitWebViewPrivate* priv = m_webView->priv;
850 g_signal_connect(priv->imContext.get(), "commit", G_CALLBACK(imContextCommitted), this);
851 g_signal_connect(priv->imContext.get(), "preedit-changed", G_CALLBACK(imContextPreeditChanged), this);
852
853 g_signal_connect(m_nativeWidget.get(), "backspace", G_CALLBACK(backspaceCallback), this);
854 g_signal_connect(m_nativeWidget.get(), "cut-clipboard", G_CALLBACK(cutClipboardCallback), this);
855 g_signal_connect(m_nativeWidget.get(), "copy-clipboard", G_CALLBACK(copyClipboardCallback), this);
856 g_signal_connect(m_nativeWidget.get(), "paste-clipboard", G_CALLBACK(pasteClipboardCallback), this);
857 g_signal_connect(m_nativeWidget.get(), "select-all", G_CALLBACK(selectAllCallback), this);
858 g_signal_connect(m_nativeWidget.get(), "move-cursor", G_CALLBACK(moveCursorCallback), this);
859 g_signal_connect(m_nativeWidget.get(), "delete-from-cursor", G_CALLBACK(deleteFromCursorCallback), this);
860 g_signal_connect(m_nativeWidget.get(), "toggle-overwrite", G_CALLBACK(toggleOverwriteCallback), this);
861 g_signal_connect(m_nativeWidget.get(), "popup-menu", G_CALLBACK(popupMenuCallback), this);
862 g_signal_connect(m_nativeWidget.get(), "show-help", G_CALLBACK(showHelpCallback), this);
863 }
864
~EditorClient()865 EditorClient::~EditorClient()
866 {
867 WebKitWebViewPrivate* priv = m_webView->priv;
868 g_signal_handlers_disconnect_by_func(priv->imContext.get(), (gpointer)imContextCommitted, this);
869 g_signal_handlers_disconnect_by_func(priv->imContext.get(), (gpointer)imContextPreeditChanged, this);
870 }
871
textFieldDidBeginEditing(Element *)872 void EditorClient::textFieldDidBeginEditing(Element*)
873 {
874 }
875
textFieldDidEndEditing(Element *)876 void EditorClient::textFieldDidEndEditing(Element*)
877 {
878 }
879
textDidChangeInTextField(Element *)880 void EditorClient::textDidChangeInTextField(Element*)
881 {
882 }
883
doTextFieldCommandFromEvent(Element *,KeyboardEvent *)884 bool EditorClient::doTextFieldCommandFromEvent(Element*, KeyboardEvent*)
885 {
886 return false;
887 }
888
textWillBeDeletedInTextField(Element *)889 void EditorClient::textWillBeDeletedInTextField(Element*)
890 {
891 notImplemented();
892 }
893
textDidChangeInTextArea(Element *)894 void EditorClient::textDidChangeInTextArea(Element*)
895 {
896 notImplemented();
897 }
898
updateSpellingUIWithGrammarString(const String &,const GrammarDetail &)899 void EditorClient::updateSpellingUIWithGrammarString(const String&, const GrammarDetail&)
900 {
901 notImplemented();
902 }
903
updateSpellingUIWithMisspelledWord(const String &)904 void EditorClient::updateSpellingUIWithMisspelledWord(const String&)
905 {
906 notImplemented();
907 }
908
showSpellingUI(bool)909 void EditorClient::showSpellingUI(bool)
910 {
911 notImplemented();
912 }
913
spellingUIIsShowing()914 bool EditorClient::spellingUIIsShowing()
915 {
916 notImplemented();
917 return false;
918 }
919
920 }
921