|
| 1 | +/* |
| 2 | + * Copyright (C) 2012 Igalia S.L. |
| 3 | + * |
| 4 | + * Redistribution and use in source and binary forms, with or without |
| 5 | + * modification, are permitted provided that the following conditions |
| 6 | + * are met: |
| 7 | + * 1. Redistributions of source code must retain the above copyright |
| 8 | + * notice, this list of conditions and the following disclaimer. |
| 9 | + * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | + * notice, this list of conditions and the following disclaimer in the |
| 11 | + * documentation and/or other materials provided with the distribution. |
| 12 | + * |
| 13 | + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 14 | + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 15 | + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 17 | + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 18 | + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 19 | + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 20 | + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 21 | + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 22 | + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 23 | + * THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | + */ |
| 25 | + |
| 26 | +#include "config.h" |
| 27 | + |
| 28 | +#include "GtkInputMethodFilter.h" |
| 29 | +#include "WTFStringUtilities.h" |
| 30 | +#include <gtk/gtk.h> |
| 31 | +#include <wtf/gobject/GOwnPtr.h> |
| 32 | +#include <wtf/gobject/GRefPtr.h> |
| 33 | +#include <wtf/text/CString.h> |
| 34 | + |
| 35 | +using namespace WebCore; |
| 36 | + |
| 37 | +namespace TestWebKitAPI { |
| 38 | + |
| 39 | +class TestInputMethodFilter : public GtkInputMethodFilter { |
| 40 | +public: |
| 41 | + TestInputMethodFilter() |
| 42 | + : m_testWindow(gtk_window_new(GTK_WINDOW_POPUP)) |
| 43 | + { |
| 44 | + gtk_widget_show(m_testWindow.get()); |
| 45 | + setWidget(m_testWindow.get()); |
| 46 | + |
| 47 | + // Focus in is necessary to activate the default input method in the multicontext. |
| 48 | + notifyFocusedIn(); |
| 49 | + } |
| 50 | + |
| 51 | + Vector<String>& events() { return m_events; } |
| 52 | + |
| 53 | + void sendKeyEventToFilter(unsigned int gdkKeyValue, GdkEventType type, unsigned int modifiers = 0) |
| 54 | + { |
| 55 | + GdkEvent* event = gdk_event_new(type); |
| 56 | + event->key.keyval = gdkKeyValue; |
| 57 | + event->key.state = modifiers; |
| 58 | + event->key.window = gtk_widget_get_window(m_testWindow.get()); |
| 59 | + event->key.time = GDK_CURRENT_TIME; |
| 60 | + g_object_ref(event->key.window); |
| 61 | + |
| 62 | +#ifndef GTK_API_VERSION_2 |
| 63 | + gdk_event_set_device(event, gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gdk_display_get_default()))); |
| 64 | +#endif |
| 65 | + |
| 66 | + GOwnPtr<GdkKeymapKey> keys; |
| 67 | + gint nKeys; |
| 68 | + if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeyValue, &keys.outPtr(), &nKeys)) |
| 69 | + event->key.hardware_keycode = keys.get()[0].keycode; |
| 70 | + |
| 71 | + filterKeyEvent(&event->key); |
| 72 | + gdk_event_free(event); |
| 73 | + } |
| 74 | + |
| 75 | + void sendPressAndReleaseKeyEventPairToFilter(unsigned int gdkKeyValue, unsigned int modifiers = 0) |
| 76 | + { |
| 77 | + sendKeyEventToFilter(gdkKeyValue, GDK_KEY_PRESS, modifiers); |
| 78 | + sendKeyEventToFilter(gdkKeyValue, GDK_KEY_RELEASE, modifiers); |
| 79 | + } |
| 80 | + |
| 81 | +protected: |
| 82 | + virtual bool sendSimpleKeyEvent(GdkEventKey* event, WTF::String eventString) |
| 83 | + { |
| 84 | + const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press"; |
| 85 | + if (!eventString.isNull()) |
| 86 | + m_events.append(String::format("sendSimpleKeyEvent type=%s keycode=%x text='%s'", eventType, event->keyval, eventString.utf8().data())); |
| 87 | + else |
| 88 | + m_events.append(String::format("sendSimpleKeyEvent type=%s keycode=%x", eventType, event->keyval)); |
| 89 | + |
| 90 | + return true; |
| 91 | + } |
| 92 | + |
| 93 | + virtual bool sendKeyEventWithCompositionResults(GdkEventKey* event, ResultsToSend resultsToSend) |
| 94 | + { |
| 95 | + const char* eventType = event->type == GDK_KEY_RELEASE ? "release" : "press"; |
| 96 | + m_events.append(String::format("sendKeyEventWithCompositionResults type=%s keycode=%u", eventType, event->keyval)); |
| 97 | + |
| 98 | + if (resultsToSend & Composition && !m_confirmedComposition.isNull()) |
| 99 | + confirmCompositionText(m_confirmedComposition); |
| 100 | + if (resultsToSend & Preedit && !m_preedit.isNull()) |
| 101 | + setPreedit(m_preedit, m_cursorOffset); |
| 102 | + |
| 103 | + return true; |
| 104 | + } |
| 105 | + |
| 106 | + virtual bool canEdit() |
| 107 | + { |
| 108 | + return true; |
| 109 | + } |
| 110 | + |
| 111 | + virtual void confirmCompositionText(String text) |
| 112 | + { |
| 113 | + m_events.append(String::format("confirmComposition '%s'", text.utf8().data())); |
| 114 | + } |
| 115 | + |
| 116 | + virtual void confirmCurrentComposition() |
| 117 | + { |
| 118 | + m_events.append(String("confirmCurrentcomposition")); |
| 119 | + } |
| 120 | + |
| 121 | + virtual void cancelCurrentComposition() |
| 122 | + { |
| 123 | + m_events.append(String("cancelCurrentComposition")); |
| 124 | + } |
| 125 | + |
| 126 | + virtual void setPreedit(String preedit, int cursorOffset) |
| 127 | + { |
| 128 | + m_events.append(String::format("setPreedit text='%s' cursorOffset=%i", preedit.utf8().data(), cursorOffset)); |
| 129 | + } |
| 130 | + |
| 131 | +private: |
| 132 | + GRefPtr<GtkWidget> m_testWindow; |
| 133 | + Vector<String> m_events; |
| 134 | +}; |
| 135 | + |
| 136 | +TEST(GTK, GtkInputMethodFilterSimple) |
| 137 | +{ |
| 138 | + TestInputMethodFilter inputMethodFilter; |
| 139 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_g); |
| 140 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_t); |
| 141 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_k); |
| 142 | + |
| 143 | + const Vector<String>& events = inputMethodFilter.events(); |
| 144 | + |
| 145 | + ASSERT_EQ(6, events.size()); |
| 146 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=67 text='g'"), events[0]); |
| 147 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=67"), events[1]); |
| 148 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=74 text='t'"), events[2]); |
| 149 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=74"), events[3]); |
| 150 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=6b text='k'"), events[4]); |
| 151 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=6b"), events[5]); |
| 152 | +} |
| 153 | + |
| 154 | +TEST(GTK, GtkInputMethodFilterUnicodeSequence) |
| 155 | +{ |
| 156 | + TestInputMethodFilter inputMethodFilter; |
| 157 | + |
| 158 | + // This is simple unicode hex entry of the characters, u, 0, 0, f, 4 pressed with |
| 159 | + // the shift and controls keys held down. In reality, these values are not typical |
| 160 | + // of an actual hex entry, because they'd be transformed by the shift modifier according |
| 161 | + // to the keyboard layout. For instance, on a US keyboard a 0 with the shift key pressed |
| 162 | + // is a right parenthesis. Using these values prevents having to work out what the |
| 163 | + // transformed characters are based on the current keyboard layout. |
| 164 | + inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Control_L, GDK_KEY_PRESS); |
| 165 | + inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Shift_L, GDK_KEY_PRESS, GDK_CONTROL_MASK); |
| 166 | + |
| 167 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_U, GDK_SHIFT_MASK | GDK_CONTROL_MASK); |
| 168 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_0, GDK_SHIFT_MASK | GDK_CONTROL_MASK); |
| 169 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_0, GDK_SHIFT_MASK | GDK_CONTROL_MASK); |
| 170 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_F, GDK_SHIFT_MASK | GDK_CONTROL_MASK); |
| 171 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_4, GDK_SHIFT_MASK | GDK_CONTROL_MASK); |
| 172 | + |
| 173 | + inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Shift_L, GDK_KEY_RELEASE, GDK_CONTROL_MASK | GDK_SHIFT_MASK); |
| 174 | + inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Control_L, GDK_KEY_RELEASE, GDK_CONTROL_MASK); |
| 175 | + |
| 176 | + const Vector<String>& events = inputMethodFilter.events(); |
| 177 | + ASSERT_EQ(21, events.size()); |
| 178 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=ffe3"), events[0]); |
| 179 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=ffe1"), events[1]); |
| 180 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=85"), events[2]); |
| 181 | + ASSERT_EQ(String("setPreedit text='u' cursorOffset=1"), events[3]); |
| 182 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=55"), events[4]); |
| 183 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=48"), events[5]); |
| 184 | + ASSERT_EQ(String("setPreedit text='u0' cursorOffset=2"), events[6]); |
| 185 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=30"), events[7]); |
| 186 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=48"), events[8]); |
| 187 | + ASSERT_EQ(String("setPreedit text='u00' cursorOffset=3"), events[9]); |
| 188 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=30"), events[10]); |
| 189 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=70"), events[11]); |
| 190 | + ASSERT_EQ(String("setPreedit text='u00F' cursorOffset=4"), events[12]); |
| 191 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=46"), events[13]); |
| 192 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=52"), events[14]); |
| 193 | + ASSERT_EQ(String("setPreedit text='u00F4' cursorOffset=5"), events[15]); |
| 194 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=34"), events[16]); |
| 195 | + ASSERT_EQ(String("confirmComposition 'ô'"), events[17]); |
| 196 | + ASSERT_EQ(String("setPreedit text='' cursorOffset=0"), events[18]); |
| 197 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=ffe1"), events[19]); |
| 198 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=ffe3"), events[20]); |
| 199 | +} |
| 200 | + |
| 201 | +TEST(GTK, GtkInputMethodFilterComposeKey) |
| 202 | +{ |
| 203 | + TestInputMethodFilter inputMethodFilter; |
| 204 | + |
| 205 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key); |
| 206 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe); |
| 207 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_o); |
| 208 | + |
| 209 | + const Vector<String>& events = inputMethodFilter.events(); |
| 210 | + ASSERT_EQ(5, events.size()); |
| 211 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=39"), events[0]); |
| 212 | + ASSERT_EQ(String("setPreedit text='' cursorOffset=0"), events[1]); |
| 213 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=27"), events[2]); |
| 214 | + ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=6f text='ó'"), events[3]); |
| 215 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=6f"), events[4]); |
| 216 | +} |
| 217 | + |
| 218 | +typedef void (*GetPreeditStringCallback) (GtkIMContext*, gchar**, PangoAttrList**, int*); |
| 219 | +static void temporaryGetPreeditStringOverride(GtkIMContext*, char** string, PangoAttrList** attrs, int* cursorPosition) |
| 220 | +{ |
| 221 | + *string = g_strdup("preedit of doom, bringer of cheese"); |
| 222 | + *cursorPosition = 3; |
| 223 | +} |
| 224 | + |
| 225 | +TEST(GTK, GtkInputMethodFilterContextEventsWithoutKeyEvents) |
| 226 | +{ |
| 227 | + TestInputMethodFilter inputMethodFilter; |
| 228 | + |
| 229 | + // This is a bit of a hack to avoid mocking out the entire InputMethodContext, by |
| 230 | + // simply replacing the get_preedit_string virtual method for the length of this test. |
| 231 | + GtkIMContext* context = inputMethodFilter.context(); |
| 232 | + GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context); |
| 233 | + GetPreeditStringCallback previousCallback = contextClass->get_preedit_string; |
| 234 | + contextClass->get_preedit_string = temporaryGetPreeditStringOverride; |
| 235 | + |
| 236 | + g_signal_emit_by_name(context, "preedit-changed"); |
| 237 | + g_signal_emit_by_name(context, "commit", "commit text"); |
| 238 | + |
| 239 | + contextClass->get_preedit_string = previousCallback; |
| 240 | + |
| 241 | + const Vector<String>& events = inputMethodFilter.events(); |
| 242 | + ASSERT_EQ(6, events.size()); |
| 243 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=16777215"), events[0]); |
| 244 | + ASSERT_EQ(String("setPreedit text='preedit of doom, bringer of cheese' cursorOffset=3"), events[1]); |
| 245 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=ffffff"), events[2]); |
| 246 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=16777215"), events[3]); |
| 247 | + ASSERT_EQ(String("confirmComposition 'commit text'"), events[4]); |
| 248 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=ffffff"), events[5]); |
| 249 | +} |
| 250 | + |
| 251 | +static bool gSawContextReset = false; |
| 252 | +typedef void (*ResetCallback) (GtkIMContext*); |
| 253 | +static void temporaryResetOverride(GtkIMContext*) |
| 254 | +{ |
| 255 | + gSawContextReset = true; |
| 256 | +} |
| 257 | + |
| 258 | +static void verifyCanceledComposition(const Vector<String>& events) |
| 259 | +{ |
| 260 | + ASSERT_EQ(4, events.size()); |
| 261 | + ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=39"), events[0]); |
| 262 | + ASSERT_EQ(String("setPreedit text='' cursorOffset=0"), events[1]); |
| 263 | + ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=27"), events[2]); |
| 264 | + ASSERT_EQ(String("confirmCurrentcomposition"), events[3]); |
| 265 | + ASSERT(gSawContextReset); |
| 266 | +} |
| 267 | + |
| 268 | +TEST(GTK, GtkInputMethodFilterContextFocusOutDuringOngoingComposition) |
| 269 | +{ |
| 270 | + TestInputMethodFilter inputMethodFilter; |
| 271 | + |
| 272 | + // See comment above about this technique. |
| 273 | + GtkIMContext* context = inputMethodFilter.context(); |
| 274 | + GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context); |
| 275 | + ResetCallback previousCallback = contextClass->reset; |
| 276 | + contextClass->reset = temporaryResetOverride; |
| 277 | + |
| 278 | + gSawContextReset = false; |
| 279 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key); |
| 280 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe); |
| 281 | + inputMethodFilter.notifyFocusedOut(); |
| 282 | + |
| 283 | + verifyCanceledComposition(inputMethodFilter.events()); |
| 284 | + |
| 285 | + contextClass->reset = previousCallback; |
| 286 | +} |
| 287 | + |
| 288 | +TEST(GTK, GtkInputMethodFilterContextMouseClickDuringOngoingComposition) |
| 289 | +{ |
| 290 | + TestInputMethodFilter inputMethodFilter; |
| 291 | + |
| 292 | + // See comment above about this technique. |
| 293 | + GtkIMContext* context = inputMethodFilter.context(); |
| 294 | + GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context); |
| 295 | + ResetCallback previousCallback = contextClass->reset; |
| 296 | + contextClass->reset = temporaryResetOverride; |
| 297 | + |
| 298 | + gSawContextReset = false; |
| 299 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key); |
| 300 | + inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe); |
| 301 | + inputMethodFilter.notifyMouseButtonPress(); |
| 302 | + |
| 303 | + verifyCanceledComposition(inputMethodFilter.events()); |
| 304 | + |
| 305 | + contextClass->reset = previousCallback; |
| 306 | +} |
| 307 | + |
| 308 | +} // namespace TestWebKitAPI |
0 commit comments