Unreviewed, rolling out r234489.
[WebKit-https.git] / Source / WebKit / UIProcess / gtk / InputMethodFilter.cpp
1 /*
2  * Copyright (C) 2012, 2014 Igalia S.L.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  */
19
20 #include "config.h"
21 #include "InputMethodFilter.h"
22
23 #include "NativeWebKeyboardEvent.h"
24 #include "WebPageProxy.h"
25 #include <WebCore/Color.h>
26 #include <WebCore/CompositionResults.h>
27 #include <WebCore/Editor.h>
28 #include <WebCore/GUniquePtrGtk.h>
29 #include <WebCore/IntRect.h>
30 #include <gdk/gdkkeysyms.h>
31 #include <gtk/gtk.h>
32 #include <wtf/Vector.h>
33 #include <wtf/glib/GUniquePtr.h>
34
35 namespace WebKit {
36 using namespace WebCore;
37
38 void InputMethodFilter::handleCommitCallback(InputMethodFilter* filter, const char* compositionString)
39 {
40     filter->handleCommit(compositionString);
41 }
42
43 void InputMethodFilter::handlePreeditStartCallback(InputMethodFilter* filter)
44 {
45     filter->handlePreeditStart();
46 }
47
48 void InputMethodFilter::handlePreeditChangedCallback(InputMethodFilter* filter)
49 {
50     filter->handlePreeditChanged();
51 }
52
53 void InputMethodFilter::handlePreeditEndCallback(InputMethodFilter* filter)
54 {
55     filter->handlePreeditEnd();
56 }
57
58 InputMethodFilter::InputMethodFilter()
59     : m_context(adoptGRef(gtk_im_multicontext_new()))
60     , m_page(nullptr)
61     , m_enabled(false)
62     , m_composingTextCurrently(false)
63     , m_filteringKeyEvent(false)
64     , m_preeditChanged(false)
65     , m_preventNextCommit(false)
66     , m_justSentFakeKeyUp(false)
67     , m_cursorOffset(0)
68     , m_lastFilteredKeyPressCodeWithNoResults(GDK_KEY_VoidSymbol)
69 #if ENABLE(API_TESTS)
70     , m_testingMode(false)
71 #endif
72 {
73     g_signal_connect_swapped(m_context.get(), "commit", G_CALLBACK(handleCommitCallback), this);
74     g_signal_connect_swapped(m_context.get(), "preedit-start", G_CALLBACK(handlePreeditStartCallback), this);
75     g_signal_connect_swapped(m_context.get(), "preedit-changed", G_CALLBACK(handlePreeditChangedCallback), this);
76     g_signal_connect_swapped(m_context.get(), "preedit-end", G_CALLBACK(handlePreeditEndCallback), this);
77 }
78
79 InputMethodFilter::~InputMethodFilter()
80 {
81     g_signal_handlers_disconnect_matched(m_context.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
82 }
83
84 bool InputMethodFilter::isViewFocused() const
85 {
86 #if ENABLE(API_TESTS)
87     ASSERT(m_page || m_testingMode);
88     if (m_testingMode)
89         return true;
90 #else
91     ASSERT(m_page);
92 #endif
93     return m_page->isViewFocused();
94 }
95
96 void InputMethodFilter::setEnabled(bool enabled)
97 {
98 #if ENABLE(API_TESTS)
99     ASSERT(m_page || m_testingMode);
100 #else
101     ASSERT(m_page);
102 #endif
103
104     // Notify focus out before changing the m_enabled.
105     if (!enabled)
106         notifyFocusedOut();
107     m_enabled = enabled;
108     if (enabled && isViewFocused())
109         notifyFocusedIn();
110 }
111
112 void InputMethodFilter::setCursorRect(const IntRect& cursorRect)
113 {
114     ASSERT(m_page);
115
116     if (!m_enabled)
117         return;
118
119     // Don't move the window unless the cursor actually moves more than 10
120     // pixels. This prevents us from making the window flash during minor
121     // cursor adjustments.
122     static const int windowMovementThreshold = 10 * 10;
123     if (cursorRect.location().distanceSquaredToPoint(m_lastCareLocation) < windowMovementThreshold)
124         return;
125
126     m_lastCareLocation = cursorRect.location();
127     IntRect translatedRect = cursorRect;
128
129     GtkAllocation allocation;
130     gtk_widget_get_allocation(m_page->viewWidget(), &allocation);
131     translatedRect.move(allocation.x, allocation.y);
132
133     GdkRectangle gdkCursorRect = translatedRect;
134     gtk_im_context_set_cursor_location(m_context.get(), &gdkCursorRect);
135 }
136
137 void InputMethodFilter::handleKeyboardEvent(GdkEventKey* event, const String& simpleString, EventFakedForComposition faked)
138 {
139 #if ENABLE(API_TESTS)
140     if (m_testingMode) {
141         logHandleKeyboardEventForTesting(event, simpleString, faked);
142         return;
143     }
144 #endif
145
146     if (m_filterKeyEventCompletionHandler) {
147         m_filterKeyEventCompletionHandler(CompositionResults(simpleString), faked);
148         m_filterKeyEventCompletionHandler = nullptr;
149     } else
150         m_page->handleKeyboardEvent(NativeWebKeyboardEvent(reinterpret_cast<GdkEvent*>(event), CompositionResults(simpleString), faked, Vector<String>()));
151 }
152
153 void InputMethodFilter::handleKeyboardEventWithCompositionResults(GdkEventKey* event, ResultsToSend resultsToSend, EventFakedForComposition faked)
154 {
155 #if ENABLE(API_TESTS)
156     if (m_testingMode) {
157         logHandleKeyboardEventWithCompositionResultsForTesting(event, resultsToSend, faked);
158         return;
159     }
160 #endif
161
162     if (m_filterKeyEventCompletionHandler) {
163         m_filterKeyEventCompletionHandler(CompositionResults(CompositionResults::WillSendCompositionResultsSoon), faked);
164         m_filterKeyEventCompletionHandler = nullptr;
165     } else
166         m_page->handleKeyboardEvent(NativeWebKeyboardEvent(reinterpret_cast<GdkEvent*>(event), CompositionResults(CompositionResults::WillSendCompositionResultsSoon), faked, Vector<String>()));
167     if (resultsToSend & Composition && !m_confirmedComposition.isNull())
168         m_page->confirmComposition(m_confirmedComposition, -1, 0);
169
170     if (resultsToSend & Preedit && !m_preedit.isNull()) {
171         m_page->setComposition(m_preedit, Vector<CompositionUnderline> { CompositionUnderline(0, m_preedit.length(), CompositionUnderlineColor::TextColor, Color(Color::black), false) },
172             m_cursorOffset, m_cursorOffset, 0 /* replacement start */, 0 /* replacement end */);
173     }
174 }
175
176 void InputMethodFilter::filterKeyEvent(GdkEventKey* event, FilterKeyEventCompletionHandler&& completionHandler)
177 {
178 #if ENABLE(API_TESTS)
179     ASSERT(m_page || m_testingMode);
180 #else
181     ASSERT(m_page);
182 #endif
183     m_filterKeyEventCompletionHandler = WTFMove(completionHandler);
184     if (!m_enabled) {
185         handleKeyboardEvent(event);
186         return;
187     }
188
189     m_preeditChanged = false;
190     m_filteringKeyEvent = true;
191
192     unsigned lastFilteredKeyPressCodeWithNoResults = m_lastFilteredKeyPressCodeWithNoResults;
193     m_lastFilteredKeyPressCodeWithNoResults = GDK_KEY_VoidSymbol;
194
195     bool filtered = gtk_im_context_filter_keypress(m_context.get(), event);
196     m_filteringKeyEvent = false;
197
198     bool justSentFakeKeyUp = m_justSentFakeKeyUp;
199     m_justSentFakeKeyUp = false;
200     if (justSentFakeKeyUp && event->type == GDK_KEY_RELEASE)
201         return;
202
203     // Simple input methods work such that even normal keystrokes fire the
204     // commit signal. We detect those situations and treat them as normal
205     // key events, supplying the commit string as the key character.
206     if (filtered && !m_composingTextCurrently && !m_preeditChanged && m_confirmedComposition.length() == 1) {
207         handleKeyboardEvent(event, m_confirmedComposition);
208         m_confirmedComposition = String();
209         return;
210     }
211
212     if (filtered && event->type == GDK_KEY_PRESS) {
213         if (!m_preeditChanged && m_confirmedComposition.isNull()) {
214             m_composingTextCurrently = true;
215             m_lastFilteredKeyPressCodeWithNoResults = event->keyval;
216             return;
217         }
218
219         handleKeyboardEventWithCompositionResults(event);
220         if (!m_confirmedComposition.isEmpty()) {
221             m_composingTextCurrently = false;
222             m_confirmedComposition = String();
223         }
224         return;
225     }
226
227     // If we previously filtered a key press event and it yielded no results. Suppress
228     // the corresponding key release event to avoid confusing the web content.
229     if (event->type == GDK_KEY_RELEASE && lastFilteredKeyPressCodeWithNoResults == event->keyval)
230         return;
231
232     // At this point a keystroke was either:
233     // 1. Unfiltered
234     // 2. A filtered keyup event. As the IME code in EditorClient.h doesn't
235     //    ever look at keyup events, we send any composition results before
236     //    the key event.
237     // Both might have composition results or not.
238     //
239     // It's important to send the composition results before the event
240     // because some IM modules operate that way. For example (taken from
241     // the Chromium source), the latin-post input method gives this sequence
242     // when you press 'a' and then backspace:
243     //  1. keydown 'a' (filtered)
244     //  2. preedit changed to "a"
245     //  3. keyup 'a' (unfiltered)
246     //  4. keydown Backspace (unfiltered)
247     //  5. commit "a"
248     //  6. preedit end
249     if (!m_confirmedComposition.isEmpty())
250         confirmComposition();
251     if (m_preeditChanged)
252         updatePreedit();
253     handleKeyboardEvent(event);
254 }
255
256 void InputMethodFilter::confirmComposition()
257 {
258 #if ENABLE(API_TESTS)
259     if (m_testingMode) {
260         logConfirmCompositionForTesting();
261         m_confirmedComposition = String();
262         return;
263     }
264 #endif
265     m_page->confirmComposition(m_confirmedComposition, -1, 0);
266     m_confirmedComposition = String();
267 }
268
269 void InputMethodFilter::updatePreedit()
270 {
271 #if ENABLE(API_TESTS)
272     if (m_testingMode) {
273         logSetPreeditForTesting();
274         return;
275     }
276 #endif
277     // FIXME: We should parse the PangoAttrList that we get from the IM context here.
278     m_page->setComposition(m_preedit, Vector<CompositionUnderline> { CompositionUnderline(0, m_preedit.length(), CompositionUnderlineColor::TextColor, Color(Color::black), false) },
279         m_cursorOffset, m_cursorOffset, 0 /* replacement start */, 0 /* replacement end */);
280     m_preeditChanged = false;
281 }
282
283 void InputMethodFilter::notifyFocusedIn()
284 {
285 #if ENABLE(API_TESTS)
286     ASSERT(m_page || m_testingMode);
287 #else
288     ASSERT(m_page);
289 #endif
290     if (!m_enabled)
291         return;
292
293     gtk_im_context_focus_in(m_context.get());
294 }
295
296 void InputMethodFilter::notifyFocusedOut()
297 {
298 #if ENABLE(API_TESTS)
299     ASSERT(m_page || m_testingMode);
300 #else
301     ASSERT(m_page);
302 #endif
303     if (!m_enabled)
304         return;
305
306     confirmCurrentComposition();
307     cancelContextComposition();
308     gtk_im_context_focus_out(m_context.get());
309 }
310
311 void InputMethodFilter::notifyMouseButtonPress()
312 {
313 #if ENABLE(API_TESTS)
314     ASSERT(m_page || m_testingMode);
315 #else
316     ASSERT(m_page);
317 #endif
318
319     // Confirming the composition may trigger a selection change, which
320     // might trigger further unwanted actions on the context, so we prevent
321     // that by setting m_composingTextCurrently to false.
322     confirmCurrentComposition();
323     cancelContextComposition();
324 }
325
326 void InputMethodFilter::confirmCurrentComposition()
327 {
328     if (!m_composingTextCurrently)
329         return;
330
331 #if ENABLE(API_TESTS)
332     if (m_testingMode) {
333         m_composingTextCurrently = false;
334         return;
335     }
336 #endif
337
338     m_page->confirmComposition(String(), -1, 0);
339     m_composingTextCurrently = false;
340 }
341
342 void InputMethodFilter::cancelContextComposition()
343 {
344     m_preventNextCommit = !m_preedit.isEmpty();
345
346     gtk_im_context_reset(m_context.get());
347
348     m_composingTextCurrently = false;
349     m_justSentFakeKeyUp = false;
350     m_preedit = String();
351     m_confirmedComposition = String();
352 }
353
354 void InputMethodFilter::sendCompositionAndPreeditWithFakeKeyEvents(ResultsToSend resultsToSend)
355 {
356     // The Windows composition key event code is 299 or VK_PROCESSKEY. We need to
357     // emit this code for web compatibility reasons when key events trigger
358     // composition results. GDK doesn't have an equivalent, so we send VoidSymbol
359     // here to WebCore. PlatformKeyEvent knows to convert this code into
360     // VK_PROCESSKEY.
361     static const int compositionEventKeyCode = GDK_KEY_VoidSymbol;
362
363     GUniquePtr<GdkEvent> event(gdk_event_new(GDK_KEY_PRESS));
364     event->key.time = GDK_CURRENT_TIME;
365     event->key.keyval = compositionEventKeyCode;
366     handleKeyboardEventWithCompositionResults(&event->key, resultsToSend, EventFaked);
367
368     m_confirmedComposition = String();
369     if (resultsToSend & Composition)
370         m_composingTextCurrently = false;
371
372     event->type = GDK_KEY_RELEASE;
373     handleKeyboardEvent(&event->key, String(), EventFaked);
374     m_justSentFakeKeyUp = true;
375 }
376
377 void InputMethodFilter::handleCommit(const char* compositionString)
378 {
379     if (m_preventNextCommit) {
380         m_preventNextCommit = false;
381         return;
382     }
383
384     if (!m_enabled)
385         return;
386
387     m_confirmedComposition.append(String::fromUTF8(compositionString));
388
389     // If the commit was triggered outside of a key event, just send
390     // the IME event now. If we are handling a key event, we'll decide
391     // later how to handle this.
392     if (!m_filteringKeyEvent)
393         sendCompositionAndPreeditWithFakeKeyEvents(Composition);
394 }
395
396 void InputMethodFilter::handlePreeditStart()
397 {
398     if (m_preventNextCommit || !m_enabled)
399         return;
400     m_preeditChanged = true;
401     m_preedit = emptyString();
402 }
403
404 void InputMethodFilter::handlePreeditChanged()
405 {
406     if (!m_enabled)
407         return;
408
409     GUniqueOutPtr<gchar> newPreedit;
410     gtk_im_context_get_preedit_string(m_context.get(), &newPreedit.outPtr(), nullptr, &m_cursorOffset);
411
412     if (m_preventNextCommit) {
413         if (strlen(newPreedit.get()) > 0)
414             m_preventNextCommit = false;
415         else
416             return;
417     }
418
419     m_preedit = String::fromUTF8(newPreedit.get());
420     m_cursorOffset = std::min(std::max(m_cursorOffset, 0), static_cast<int>(m_preedit.length()));
421
422     m_composingTextCurrently = !m_preedit.isEmpty();
423     m_preeditChanged = true;
424
425     if (!m_filteringKeyEvent)
426         sendCompositionAndPreeditWithFakeKeyEvents(Preedit);
427 }
428
429 void InputMethodFilter::handlePreeditEnd()
430 {
431     if (m_preventNextCommit || !m_enabled)
432         return;
433
434     m_preedit = String();
435     m_cursorOffset = 0;
436     m_preeditChanged = true;
437
438     if (!m_filteringKeyEvent)
439         updatePreedit();
440 }
441
442 #if ENABLE(API_TESTS)
443 void InputMethodFilter::logHandleKeyboardEventForTesting(GdkEventKey* event, const String& eventString, EventFakedForComposition faked)
444 {
445     const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press";
446     const char* fakedString = faked == EventFaked ? " (faked)" : "";
447     if (!eventString.isNull())
448         m_events.append(String::format("sendSimpleKeyEvent type=%s keycode=%x text='%s'%s", eventType, event->keyval, eventString.utf8().data(), fakedString));
449     else
450         m_events.append(String::format("sendSimpleKeyEvent type=%s keycode=%x%s", eventType, event->keyval, fakedString));
451 }
452
453 void InputMethodFilter::logHandleKeyboardEventWithCompositionResultsForTesting(GdkEventKey* event, ResultsToSend resultsToSend, EventFakedForComposition faked)
454 {
455     const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press";
456     const char* fakedString = faked == EventFaked ? " (faked)" : "";
457     m_events.append(String::format("sendKeyEventWithCompositionResults type=%s keycode=%x%s", eventType, event->keyval, fakedString));
458
459     if (resultsToSend & Composition && !m_confirmedComposition.isNull())
460         logConfirmCompositionForTesting();
461     if (resultsToSend & Preedit && !m_preedit.isNull())
462         logSetPreeditForTesting();
463 }
464
465 void InputMethodFilter::logConfirmCompositionForTesting()
466 {
467     if (m_confirmedComposition.isEmpty())
468         m_events.append(String("confirmCurrentcomposition"));
469     else
470         m_events.append(String::format("confirmComposition '%s'", m_confirmedComposition.utf8().data()));
471 }
472
473 void InputMethodFilter::logSetPreeditForTesting()
474 {
475     m_events.append(String::format("setPreedit text='%s' cursorOffset=%i", m_preedit.utf8().data(), m_cursorOffset));
476 }
477 #endif // ENABLE(API_TESTS)
478
479 } // namespace WebKit