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