1/* 2 * Copyright (C) 2012 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 Lesser 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 * Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public 15 * License along with this library; if not, write to the Free Software 16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 */ 18 19#include "config.h" 20#include "GtkInputMethodFilter.h" 21 22#include "GUniquePtrGtk.h" 23#include "GtkVersioning.h" 24#include <gdk/gdkkeysyms.h> 25#include <gtk/gtk.h> 26#include <wtf/MathExtras.h> 27#include <wtf/gobject/GUniquePtr.h> 28 29// The Windows composition key event code is 299 or VK_PROCESSKEY. We need to 30// emit this code for web compatibility reasons when key events trigger 31// composition results. GDK doesn't have an equivalent, so we send VoidSymbol 32// here to WebCore. PlatformKeyEvent knows to convert this code into 33// VK_PROCESSKEY. 34const int gCompositionEventKeyCode = GDK_KEY_VoidSymbol; 35 36namespace WebCore { 37 38static void handleCommitCallback(GtkIMContext*, const char* compositionString, GtkInputMethodFilter* filter) 39{ 40 filter->handleCommit(compositionString); 41} 42 43static void handlePreeditStartCallback(GtkIMContext*, GtkInputMethodFilter* filter) 44{ 45 filter->handlePreeditStart(); 46} 47 48static void handlePreeditChangedCallback(GtkIMContext*, GtkInputMethodFilter* filter) 49{ 50 filter->handlePreeditChanged(); 51} 52 53static void handlePreeditEndCallback(GtkIMContext*, GtkInputMethodFilter* filter) 54{ 55 filter->handlePreeditEnd(); 56} 57 58static void handleWidgetRealize(GtkWidget* widget, GtkInputMethodFilter* filter) 59{ 60 GdkWindow* window = gtk_widget_get_window(widget); 61 ASSERT(window); 62 gtk_im_context_set_client_window(filter->context(), window); 63} 64 65void GtkInputMethodFilter::setWidget(GtkWidget* widget) 66{ 67 ASSERT(!m_widget); 68 69 m_widget = widget; 70 if (gtk_widget_get_window(m_widget)) 71 handleWidgetRealize(m_widget, this); 72 else 73 g_signal_connect_after(widget, "realize", G_CALLBACK(handleWidgetRealize), this); 74} 75 76void GtkInputMethodFilter::setCursorRect(const IntRect& cursorRect) 77{ 78 // Don't move the window unless the cursor actually moves more than 10 79 // pixels. This prevents us from making the window flash during minor 80 // cursor adjustments. 81 static const int windowMovementThreshold = 10 * 10; 82 if (cursorRect.location().distanceSquaredToPoint(m_lastCareLocation) < windowMovementThreshold) 83 return; 84 85 m_lastCareLocation = cursorRect.location(); 86 IntRect translatedRect = cursorRect; 87 88 ASSERT(m_widget); 89 GtkAllocation allocation; 90 gtk_widget_get_allocation(m_widget, &allocation); 91 translatedRect.move(allocation.x, allocation.y); 92 93 GdkRectangle gdkCursorRect = cursorRect; 94 gtk_im_context_set_cursor_location(m_context.get(), &gdkCursorRect); 95} 96 97GtkInputMethodFilter::GtkInputMethodFilter() 98 : m_cursorOffset(0) 99 , m_context(adoptGRef(gtk_im_multicontext_new())) 100 , m_widget(0) 101 , m_enabled(false) 102 , m_composingTextCurrently(false) 103 , m_filteringKeyEvent(false) 104 , m_preeditChanged(false) 105 , m_preventNextCommit(false) 106 , m_justSentFakeKeyUp(false) 107 , m_lastFilteredKeyPressCodeWithNoResults(GDK_KEY_VoidSymbol) 108{ 109 g_signal_connect(m_context.get(), "commit", G_CALLBACK(handleCommitCallback), this); 110 g_signal_connect(m_context.get(), "preedit-start", G_CALLBACK(handlePreeditStartCallback), this); 111 g_signal_connect(m_context.get(), "preedit-changed", G_CALLBACK(handlePreeditChangedCallback), this); 112 g_signal_connect(m_context.get(), "preedit-end", G_CALLBACK(handlePreeditEndCallback), this); 113} 114 115GtkInputMethodFilter::~GtkInputMethodFilter() 116{ 117 g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handleCommitCallback), this); 118 g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditStartCallback), this); 119 g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditChangedCallback), this); 120 g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditEndCallback), this); 121 g_signal_handlers_disconnect_by_func(m_widget, reinterpret_cast<void*>(handleWidgetRealize), this); 122} 123 124void GtkInputMethodFilter::setEnabled(bool enabled) 125{ 126 m_enabled = enabled; 127 if (enabled) 128 gtk_im_context_focus_in(m_context.get()); 129 else 130 gtk_im_context_focus_out(m_context.get()); 131} 132 133bool GtkInputMethodFilter::filterKeyEvent(GdkEventKey* event) 134{ 135 if (!canEdit() || !m_enabled) 136 return sendSimpleKeyEvent(event); 137 138 m_preeditChanged = false; 139 m_filteringKeyEvent = true; 140 141 unsigned int lastFilteredKeyPressCodeWithNoResults = m_lastFilteredKeyPressCodeWithNoResults; 142 m_lastFilteredKeyPressCodeWithNoResults = GDK_KEY_VoidSymbol; 143 144 bool filtered = gtk_im_context_filter_keypress(m_context.get(), event); 145 m_filteringKeyEvent = false; 146 147 bool justSentFakeKeyUp = m_justSentFakeKeyUp; 148 m_justSentFakeKeyUp = false; 149 if (justSentFakeKeyUp && event->type == GDK_KEY_RELEASE) 150 return true; 151 152 // Simple input methods work such that even normal keystrokes fire the 153 // commit signal. We detect those situations and treat them as normal 154 // key events, supplying the commit string as the key character. 155 if (filtered && !m_composingTextCurrently && !m_preeditChanged && m_confirmedComposition.length() == 1) { 156 bool result = sendSimpleKeyEvent(event, m_confirmedComposition); 157 m_confirmedComposition = String(); 158 return result; 159 } 160 161 if (filtered && event->type == GDK_KEY_PRESS) { 162 if (!m_preeditChanged && m_confirmedComposition.isNull()) { 163 m_composingTextCurrently = true; 164 m_lastFilteredKeyPressCodeWithNoResults = event->keyval; 165 return true; 166 } 167 168 bool result = sendKeyEventWithCompositionResults(event); 169 if (!m_confirmedComposition.isEmpty()) { 170 m_composingTextCurrently = false; 171 m_confirmedComposition = String(); 172 } 173 return result; 174 } 175 176 // If we previously filtered a key press event and it yielded no results. Suppress 177 // the corresponding key release event to avoid confusing the web content. 178 if (event->type == GDK_KEY_RELEASE && lastFilteredKeyPressCodeWithNoResults == event->keyval) 179 return true; 180 181 // At this point a keystroke was either: 182 // 1. Unfiltered 183 // 2. A filtered keyup event. As the IME code in EditorClient.h doesn't 184 // ever look at keyup events, we send any composition results before 185 // the key event. 186 // Both might have composition results or not. 187 // 188 // It's important to send the composition results before the event 189 // because some IM modules operate that way. For example (taken from 190 // the Chromium source), the latin-post input method gives this sequence 191 // when you press 'a' and then backspace: 192 // 1. keydown 'a' (filtered) 193 // 2. preedit changed to "a" 194 // 3. keyup 'a' (unfiltered) 195 // 4. keydown Backspace (unfiltered) 196 // 5. commit "a" 197 // 6. preedit end 198 if (!m_confirmedComposition.isEmpty()) 199 confirmComposition(); 200 if (m_preeditChanged) 201 updatePreedit(); 202 return sendSimpleKeyEvent(event); 203} 204 205void GtkInputMethodFilter::notifyMouseButtonPress() 206{ 207 // Confirming the composition may trigger a selection change, which 208 // might trigger further unwanted actions on the context, so we prevent 209 // that by setting m_composingTextCurrently to false. 210 if (m_composingTextCurrently) 211 confirmCurrentComposition(); 212 m_composingTextCurrently = false; 213 cancelContextComposition(); 214} 215 216void GtkInputMethodFilter::resetContext() 217{ 218 219 // We always cancel the current WebCore composition here, in case the 220 // composition was set outside the GTK+ IME path (via a script, for 221 // instance) and we aren't tracking it. 222 cancelCurrentComposition(); 223 224 if (!m_composingTextCurrently) 225 return; 226 m_composingTextCurrently = false; 227 cancelContextComposition(); 228} 229 230void GtkInputMethodFilter::cancelContextComposition() 231{ 232 m_preventNextCommit = !m_preedit.isEmpty(); 233 234 gtk_im_context_reset(m_context.get()); 235 236 m_composingTextCurrently = false; 237 m_justSentFakeKeyUp = false; 238 m_preedit = String(); 239 m_confirmedComposition = String(); 240} 241 242void GtkInputMethodFilter::notifyFocusedIn() 243{ 244 m_enabled = true; 245 gtk_im_context_focus_in(m_context.get()); 246} 247 248void GtkInputMethodFilter::notifyFocusedOut() 249{ 250 if (!m_enabled) 251 return; 252 253 if (m_composingTextCurrently) 254 confirmCurrentComposition(); 255 cancelContextComposition(); 256 gtk_im_context_focus_out(m_context.get()); 257 m_enabled = false; 258} 259 260void GtkInputMethodFilter::confirmComposition() 261{ 262 confirmCompositionText(m_confirmedComposition); 263 m_confirmedComposition = String(); 264} 265 266void GtkInputMethodFilter::updatePreedit() 267{ 268 setPreedit(m_preedit, m_cursorOffset); 269 m_preeditChanged = false; 270} 271 272void GtkInputMethodFilter::sendCompositionAndPreeditWithFakeKeyEvents(ResultsToSend resultsToSend) 273{ 274 GUniquePtr<GdkEvent> event(gdk_event_new(GDK_KEY_PRESS)); 275 event->key.time = GDK_CURRENT_TIME; 276 event->key.keyval = gCompositionEventKeyCode; 277 sendKeyEventWithCompositionResults(&event->key, resultsToSend, EventFaked); 278 279 m_confirmedComposition = String(); 280 if (resultsToSend & Composition) 281 m_composingTextCurrently = false; 282 283 event->type = GDK_KEY_RELEASE; 284 sendSimpleKeyEvent(&event->key, String(), EventFaked); 285 m_justSentFakeKeyUp = true; 286} 287 288void GtkInputMethodFilter::handleCommit(const char* compositionString) 289{ 290 if (m_preventNextCommit) { 291 m_preventNextCommit = false; 292 return; 293 } 294 295 if (!m_enabled) 296 return; 297 298 m_confirmedComposition.append(String::fromUTF8(compositionString)); 299 300 // If the commit was triggered outside of a key event, just send 301 // the IME event now. If we are handling a key event, we'll decide 302 // later how to handle this. 303 if (!m_filteringKeyEvent) 304 sendCompositionAndPreeditWithFakeKeyEvents(Composition); 305} 306 307void GtkInputMethodFilter::handlePreeditStart() 308{ 309 if (m_preventNextCommit || !m_enabled) 310 return; 311 m_preeditChanged = true; 312 m_preedit = ""; 313} 314 315void GtkInputMethodFilter::handlePreeditChanged() 316{ 317 if (!m_enabled) 318 return; 319 320 GUniqueOutPtr<gchar> newPreedit; 321 gtk_im_context_get_preedit_string(m_context.get(), &newPreedit.outPtr(), 0, &m_cursorOffset); 322 323 if (m_preventNextCommit) { 324 if (strlen(newPreedit.get()) > 0) 325 m_preventNextCommit = false; 326 else 327 return; 328 } 329 330 m_preedit = String::fromUTF8(newPreedit.get()); 331 m_cursorOffset = std::min(std::max(m_cursorOffset, 0), static_cast<int>(m_preedit.length())); 332 333 m_composingTextCurrently = !m_preedit.isEmpty(); 334 m_preeditChanged = true; 335 336 if (!m_filteringKeyEvent) 337 sendCompositionAndPreeditWithFakeKeyEvents(Preedit); 338} 339 340void GtkInputMethodFilter::handlePreeditEnd() 341{ 342 if (m_preventNextCommit || !m_enabled) 343 return; 344 345 m_preedit = String(); 346 m_cursorOffset = 0; 347 m_preeditChanged = true; 348 349 if (!m_filteringKeyEvent) 350 updatePreedit(); 351} 352 353} 354