| 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 | |