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
36namespace WebKit {
37using namespace WebCore;
38
39void InputMethodFilter::handleCommitCallback(InputMethodFilter* filter, const char* compositionString)
40{
41 filter->handleCommit(compositionString);
42}
43
44void InputMethodFilter::handlePreeditStartCallback(InputMethodFilter* filter)
45{
46 filter->handlePreeditStart();
47}
48
49void InputMethodFilter::handlePreeditChangedCallback(InputMethodFilter* filter)
50{
51 filter->handlePreeditChanged();
52}
53
54void InputMethodFilter::handlePreeditEndCallback(InputMethodFilter* filter)
55{
56 filter->handlePreeditEnd();
57}
58
59InputMethodFilter::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
80InputMethodFilter::~InputMethodFilter()
81{
82 g_signal_handlers_disconnect_matched(m_context.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
83}
84
85bool 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
97void 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
113void 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
138void 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
154void 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
177void 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 if (justSentFakeKeyUp && event->type == GDK_KEY_RELEASE)
202 return;
203
204 // Simple input methods work such that even normal keystrokes fire the
205 // commit signal. We detect those situations and treat them as normal
206 // key events, supplying the commit string as the key character.
207 if (filtered && !m_composingTextCurrently && !m_preeditChanged && m_confirmedComposition.length() == 1) {
208 handleKeyboardEvent(event, m_confirmedComposition);
209 m_confirmedComposition = String();
210 return;
211 }
212
213 if (filtered && event->type == GDK_KEY_PRESS) {
214 if (!m_preeditChanged && m_confirmedComposition.isNull()) {
215 m_composingTextCurrently = true;
216 m_lastFilteredKeyPressCodeWithNoResults = event->keyval;
217 return;
218 }
219
220 handleKeyboardEventWithCompositionResults(event);
221 if (!m_confirmedComposition.isEmpty()) {
222 m_composingTextCurrently = false;
223 m_confirmedComposition = String();
224 }
225 return;
226 }
227
228 // If we previously filtered a key press event and it yielded no results. Suppress
229 // the corresponding key release event to avoid confusing the web content.
230 if (event->type == GDK_KEY_RELEASE && lastFilteredKeyPressCodeWithNoResults == event->keyval)
231 return;
232
233 // At this point a keystroke was either:
234 // 1. Unfiltered
235 // 2. A filtered keyup event. As the IME code in EditorClient.h doesn't
236 // ever look at keyup events, we send any composition results before
237 // the key event.
238 // Both might have composition results or not.
239 //
240 // It's important to send the composition results before the event
241 // because some IM modules operate that way. For example (taken from
242 // the Chromium source), the latin-post input method gives this sequence
243 // when you press 'a' and then backspace:
244 // 1. keydown 'a' (filtered)
245 // 2. preedit changed to "a"
246 // 3. keyup 'a' (unfiltered)
247 // 4. keydown Backspace (unfiltered)
248 // 5. commit "a"
249 // 6. preedit end
250 if (!m_confirmedComposition.isEmpty())
251 confirmComposition();
252 if (m_preeditChanged)
253 updatePreedit();
254 handleKeyboardEvent(event);
255}
256
257void InputMethodFilter::confirmComposition()
258{
259#if ENABLE(API_TESTS)
260 if (m_testingMode) {
261 logConfirmCompositionForTesting();
262 m_confirmedComposition = String();
263 return;
264 }
265#endif
266 m_page->confirmComposition(m_confirmedComposition, -1, 0);
267 m_confirmedComposition = String();
268}
269
270void InputMethodFilter::updatePreedit()
271{
272#if ENABLE(API_TESTS)
273 if (m_testingMode) {
274 logSetPreeditForTesting();
275 return;
276 }
277#endif
278 // FIXME: We should parse the PangoAttrList that we get from the IM context here.
279 m_page->setComposition(m_preedit, Vector<CompositionUnderline> { CompositionUnderline(0, m_preedit.length(), CompositionUnderlineColor::TextColor, Color(Color::black), false) },
280 m_cursorOffset, m_cursorOffset, 0 /* replacement start */, 0 /* replacement end */);
281 m_preeditChanged = false;
282}
283
284void InputMethodFilter::notifyFocusedIn()
285{
286#if ENABLE(API_TESTS)
287 ASSERT(m_page || m_testingMode);
288#else
289 ASSERT(m_page);
290#endif
291 if (!m_enabled)
292 return;
293
294 gtk_im_context_focus_in(m_context.get());
295}
296
297void InputMethodFilter::notifyFocusedOut()
298{
299#if ENABLE(API_TESTS)
300 ASSERT(m_page || m_testingMode);
301#else
302 ASSERT(m_page);
303#endif
304 if (!m_enabled)
305 return;
306
307 confirmCurrentComposition();
308 cancelContextComposition();
309 gtk_im_context_focus_out(m_context.get());
310}
311
312void InputMethodFilter::notifyMouseButtonPress()
313{
314#if ENABLE(API_TESTS)
315 ASSERT(m_page || m_testingMode);
316#else
317 ASSERT(m_page);
318#endif
319
320 // Confirming the composition may trigger a selection change, which
321 // might trigger further unwanted actions on the context, so we prevent
322 // that by setting m_composingTextCurrently to false.
323 confirmCurrentComposition();
324 cancelContextComposition();
325}
326
327void InputMethodFilter::confirmCurrentComposition()
328{
329 if (!m_composingTextCurrently)
330 return;
331
332#if ENABLE(API_TESTS)
333 if (m_testingMode) {
334 m_composingTextCurrently = false;
335 return;
336 }
337#endif
338
339 m_page->confirmComposition(String(), -1, 0);
340 m_composingTextCurrently = false;
341}
342
343void InputMethodFilter::cancelContextComposition()
344{
345 m_preventNextCommit = !m_preedit.isEmpty();
346
347 gtk_im_context_reset(m_context.get());
348
349 m_composingTextCurrently = false;
350 m_justSentFakeKeyUp = false;
351 m_preedit = String();
352 m_confirmedComposition = String();
353}
354
355void InputMethodFilter::sendCompositionAndPreeditWithFakeKeyEvents(ResultsToSend resultsToSend)
356{
357 // The Windows composition key event code is 299 or VK_PROCESSKEY. We need to
358 // emit this code for web compatibility reasons when key events trigger
359 // composition results. GDK doesn't have an equivalent, so we send VoidSymbol
360 // here to WebCore. PlatformKeyEvent knows to convert this code into
361 // VK_PROCESSKEY.
362 static const int compositionEventKeyCode = GDK_KEY_VoidSymbol;
363
364 GUniquePtr<GdkEvent> event(gdk_event_new(GDK_KEY_PRESS));
365 event->key.time = GDK_CURRENT_TIME;
366 event->key.keyval = compositionEventKeyCode;
367 handleKeyboardEventWithCompositionResults(&event->key, resultsToSend, EventFaked);
368
369 m_confirmedComposition = String();
370 if (resultsToSend & Composition)
371 m_composingTextCurrently = false;
372
373 event->type = GDK_KEY_RELEASE;
374 handleKeyboardEvent(&event->key, String(), EventFaked);
375 m_justSentFakeKeyUp = true;
376}
377
378void InputMethodFilter::handleCommit(const char* compositionString)
379{
380 if (m_preventNextCommit) {
381 m_preventNextCommit = false;
382 return;
383 }
384
385 if (!m_enabled)
386 return;
387
388 m_confirmedComposition.append(String::fromUTF8(compositionString));
389
390 // If the commit was triggered outside of a key event, just send
391 // the IME event now. If we are handling a key event, we'll decide
392 // later how to handle this.
393 if (!m_filteringKeyEvent)
394 sendCompositionAndPreeditWithFakeKeyEvents(Composition);
395}
396
397void InputMethodFilter::handlePreeditStart()
398{
399 if (m_preventNextCommit || !m_enabled)
400 return;
401 m_preeditChanged = true;
402 m_preedit = emptyString();
403}
404
405void InputMethodFilter::handlePreeditChanged()
406{
407 if (!m_enabled)
408 return;
409
410 GUniqueOutPtr<gchar> newPreedit;
411 gtk_im_context_get_preedit_string(m_context.get(), &newPreedit.outPtr(), nullptr, &m_cursorOffset);
412
413 if (m_preventNextCommit) {
414 if (strlen(newPreedit.get()) > 0)
415 m_preventNextCommit = false;
416 else
417 return;
418 }
419
420 m_preedit = String::fromUTF8(newPreedit.get());
421 m_cursorOffset = std::min(std::max(m_cursorOffset, 0), static_cast<int>(m_preedit.length()));
422
423 m_composingTextCurrently = !m_preedit.isEmpty();
424 m_preeditChanged = true;
425
426 if (!m_filteringKeyEvent)
427 sendCompositionAndPreeditWithFakeKeyEvents(Preedit);
428}
429
430void InputMethodFilter::handlePreeditEnd()
431{
432 if (m_preventNextCommit || !m_enabled)
433 return;
434
435 m_preedit = String();
436 m_cursorOffset = 0;
437 m_preeditChanged = true;
438
439 if (!m_filteringKeyEvent)
440 updatePreedit();
441}
442
443#if ENABLE(API_TESTS)
444void InputMethodFilter::logHandleKeyboardEventForTesting(GdkEventKey* event, const String& eventString, EventFakedForComposition faked)
445{
446 const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press";
447 const char* fakedString = faked == EventFaked ? " (faked)" : "";
448 if (!eventString.isNull())
449 m_events.append(makeString("sendSimpleKeyEvent type=", eventType, " keycode=", hex(event->keyval), " text='", eventString, '\'', fakedString));
450 else
451 m_events.append(makeString("sendSimpleKeyEvent type=", eventType, " keycode=", hex(event->keyval), fakedString));
452}
453
454void InputMethodFilter::logHandleKeyboardEventWithCompositionResultsForTesting(GdkEventKey* event, ResultsToSend resultsToSend, EventFakedForComposition faked)
455{
456 const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press";
457 const char* fakedString = faked == EventFaked ? " (faked)" : "";
458 m_events.append(makeString("sendKeyEventWithCompositionResults type=", eventType, " keycode=", hex(event->keyval), fakedString));
459
460 if (resultsToSend & Composition && !m_confirmedComposition.isNull())
461 logConfirmCompositionForTesting();
462 if (resultsToSend & Preedit && !m_preedit.isNull())
463 logSetPreeditForTesting();
464}
465
466void InputMethodFilter::logConfirmCompositionForTesting()
467{
468 if (m_confirmedComposition.isEmpty())
469 m_events.append(String("confirmCurrentcomposition"));
470 else
471 m_events.append(makeString("confirmComposition '", m_confirmedComposition, '\''));
472}
473
474void InputMethodFilter::logSetPreeditForTesting()
475{
476 m_events.append(makeString("setPreedit text='", m_preedit, "' cursorOffset=", m_cursorOffset));
477}
478
479#endif // ENABLE(API_TESTS)
480
481} // namespace WebKit
482