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