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