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 | 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 | |
257 | void 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 | |
270 | void 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 | |
284 | void 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 | |
297 | void 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 | |
312 | void 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 | |
327 | void 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 | |
343 | void 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 | |
355 | void 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 | |
378 | void 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 | |
397 | void InputMethodFilter::handlePreeditStart() |
398 | { |
399 | if (m_preventNextCommit || !m_enabled) |
400 | return; |
401 | m_preeditChanged = true; |
402 | m_preedit = emptyString(); |
403 | } |
404 | |
405 | void 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 | |
430 | void 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) |
444 | void 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 | |
454 | void 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 | |
466 | void 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 | |
474 | void 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 | |