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