1/*
2 * Copyright (C) 2010, 2011 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 "KeyBindingTranslator.h"
21
22#include "GtkVersioning.h"
23#include <gdk/gdkkeysyms.h>
24#include <wtf/HashMap.h>
25
26namespace WebCore {
27
28typedef HashMap<int, const char*> IntConstCharHashMap;
29
30static void backspaceCallback(GtkWidget* widget, KeyBindingTranslator* translator)
31{
32    g_signal_stop_emission_by_name(widget, "backspace");
33    translator->addPendingEditorCommand("DeleteBackward");
34}
35
36static void selectAllCallback(GtkWidget* widget, gboolean select, KeyBindingTranslator* translator)
37{
38    g_signal_stop_emission_by_name(widget, "select-all");
39    translator->addPendingEditorCommand(select ? "SelectAll" : "Unselect");
40}
41
42static void cutClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
43{
44    g_signal_stop_emission_by_name(widget, "cut-clipboard");
45    translator->addPendingEditorCommand("Cut");
46}
47
48static void copyClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
49{
50    g_signal_stop_emission_by_name(widget, "copy-clipboard");
51    translator->addPendingEditorCommand("Copy");
52}
53
54static void pasteClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
55{
56    g_signal_stop_emission_by_name(widget, "paste-clipboard");
57    translator->addPendingEditorCommand("Paste");
58}
59
60static void toggleOverwriteCallback(GtkWidget* widget, KeyBindingTranslator* translator)
61{
62    g_signal_stop_emission_by_name(widget, "toggle-overwrite");
63    translator->addPendingEditorCommand("OverWrite");
64}
65
66// GTK+ will still send these signals to the web view. So we can safely stop signal
67// emission without breaking accessibility.
68static void popupMenuCallback(GtkWidget* widget, KeyBindingTranslator*)
69{
70    g_signal_stop_emission_by_name(widget, "popup-menu");
71}
72
73static void showHelpCallback(GtkWidget* widget, KeyBindingTranslator*)
74{
75    g_signal_stop_emission_by_name(widget, "show-help");
76}
77
78static const char* const gtkDeleteCommands[][2] = {
79    { "DeleteBackward",               "DeleteForward"                        }, // Characters
80    { "DeleteWordBackward",           "DeleteWordForward"                    }, // Word ends
81    { "DeleteWordBackward",           "DeleteWordForward"                    }, // Words
82    { "DeleteToBeginningOfLine",      "DeleteToEndOfLine"                    }, // Lines
83    { "DeleteToBeginningOfLine",      "DeleteToEndOfLine"                    }, // Line ends
84    { "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph"               }, // Paragraph ends
85    { "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph"               }, // Paragraphs
86    { 0,                              0                                      } // Whitespace (M-\ in Emacs)
87};
88
89static void deleteFromCursorCallback(GtkWidget* widget, GtkDeleteType deleteType, gint count, KeyBindingTranslator* translator)
90{
91    g_signal_stop_emission_by_name(widget, "delete-from-cursor");
92    int direction = count > 0 ? 1 : 0;
93
94    // Ensuring that deleteType <= G_N_ELEMENTS here results in a compiler warning
95    // that the condition is always true.
96
97    if (deleteType == GTK_DELETE_WORDS) {
98        if (!direction) {
99            translator->addPendingEditorCommand("MoveWordForward");
100            translator->addPendingEditorCommand("MoveWordBackward");
101        } else {
102            translator->addPendingEditorCommand("MoveWordBackward");
103            translator->addPendingEditorCommand("MoveWordForward");
104        }
105    } else if (deleteType == GTK_DELETE_DISPLAY_LINES) {
106        if (!direction)
107            translator->addPendingEditorCommand("MoveToBeginningOfLine");
108        else
109            translator->addPendingEditorCommand("MoveToEndOfLine");
110    } else if (deleteType == GTK_DELETE_PARAGRAPHS) {
111        if (!direction)
112            translator->addPendingEditorCommand("MoveToBeginningOfParagraph");
113        else
114            translator->addPendingEditorCommand("MoveToEndOfParagraph");
115    }
116
117    const char* rawCommand = gtkDeleteCommands[deleteType][direction];
118    if (!rawCommand)
119      return;
120
121    for (int i = 0; i < abs(count); i++)
122        translator->addPendingEditorCommand(rawCommand);
123}
124
125static const char* const gtkMoveCommands[][4] = {
126    { "MoveBackward",                                   "MoveForward",
127      "MoveBackwardAndModifySelection",                 "MoveForwardAndModifySelection"             }, // Forward/backward grapheme
128    { "MoveLeft",                                       "MoveRight",
129      "MoveBackwardAndModifySelection",                 "MoveForwardAndModifySelection"             }, // Left/right grapheme
130    { "MoveWordBackward",                               "MoveWordForward",
131      "MoveWordBackwardAndModifySelection",             "MoveWordForwardAndModifySelection"         }, // Forward/backward word
132    { "MoveUp",                                         "MoveDown",
133      "MoveUpAndModifySelection",                       "MoveDownAndModifySelection"                }, // Up/down line
134    { "MoveToBeginningOfLine",                          "MoveToEndOfLine",
135      "MoveToBeginningOfLineAndModifySelection",        "MoveToEndOfLineAndModifySelection"         }, // Up/down line ends
136    { "MoveParagraphBackward",                          "MoveParagraphForward",
137      "MoveParagraphBackwardAndModifySelection",        "MoveParagraphForwardAndModifySelection"    }, // Up/down paragraphs
138    { "MoveToBeginningOfParagraph",                     "MoveToEndOfParagraph",
139      "MoveToBeginningOfParagraphAndModifySelection",   "MoveToEndOfParagraphAndModifySelection"    }, // Up/down paragraph ends.
140    { "MovePageUp",                                     "MovePageDown",
141      "MovePageUpAndModifySelection",                   "MovePageDownAndModifySelection"            }, // Up/down page
142    { "MoveToBeginningOfDocument",                      "MoveToEndOfDocument",
143      "MoveToBeginningOfDocumentAndModifySelection",    "MoveToEndOfDocumentAndModifySelection"     }, // Begin/end of buffer
144    { 0,                                                0,
145      0,                                                0                                           } // Horizontal page movement
146};
147
148static void moveCursorCallback(GtkWidget* widget, GtkMovementStep step, gint count, gboolean extendSelection, KeyBindingTranslator* translator)
149{
150    g_signal_stop_emission_by_name(widget, "move-cursor");
151    int direction = count > 0 ? 1 : 0;
152    if (extendSelection)
153        direction += 2;
154
155    if (static_cast<unsigned>(step) >= G_N_ELEMENTS(gtkMoveCommands))
156        return;
157
158    const char* rawCommand = gtkMoveCommands[step][direction];
159    if (!rawCommand)
160        return;
161
162    for (int i = 0; i < abs(count); i++)
163        translator->addPendingEditorCommand(rawCommand);
164}
165
166KeyBindingTranslator::KeyBindingTranslator()
167    : m_nativeWidget(gtk_text_view_new())
168{
169    g_signal_connect(m_nativeWidget.get(), "backspace", G_CALLBACK(backspaceCallback), this);
170    g_signal_connect(m_nativeWidget.get(), "cut-clipboard", G_CALLBACK(cutClipboardCallback), this);
171    g_signal_connect(m_nativeWidget.get(), "copy-clipboard", G_CALLBACK(copyClipboardCallback), this);
172    g_signal_connect(m_nativeWidget.get(), "paste-clipboard", G_CALLBACK(pasteClipboardCallback), this);
173    g_signal_connect(m_nativeWidget.get(), "select-all", G_CALLBACK(selectAllCallback), this);
174    g_signal_connect(m_nativeWidget.get(), "move-cursor", G_CALLBACK(moveCursorCallback), this);
175    g_signal_connect(m_nativeWidget.get(), "delete-from-cursor", G_CALLBACK(deleteFromCursorCallback), this);
176    g_signal_connect(m_nativeWidget.get(), "toggle-overwrite", G_CALLBACK(toggleOverwriteCallback), this);
177    g_signal_connect(m_nativeWidget.get(), "popup-menu", G_CALLBACK(popupMenuCallback), this);
178    g_signal_connect(m_nativeWidget.get(), "show-help", G_CALLBACK(showHelpCallback), this);
179}
180
181struct KeyCombinationEntry {
182    unsigned gdkKeyCode;
183    unsigned state;
184    const char* name;
185};
186
187static const KeyCombinationEntry keyDownEntries[] = {
188    { GDK_b,         GDK_CONTROL_MASK,               "ToggleBold"    },
189    { GDK_i,         GDK_CONTROL_MASK,               "ToggleItalic"  },
190    { GDK_Escape,    0,                              "Cancel"        },
191    { GDK_greater,   GDK_CONTROL_MASK,               "Cancel"        },
192};
193
194// These commands are text insertion commands, so should take place
195// while handling the KeyPress event.
196static const KeyCombinationEntry keyPressEntries[] = {
197    { GDK_Tab,       0,                              "InsertTab"     },
198    { GDK_Tab,       GDK_SHIFT_MASK,                 "InsertBacktab" },
199};
200
201void KeyBindingTranslator::getEditorCommandsForKeyEvent(GdkEventKey* event, EventType type, Vector<WTF::String>& commandList)
202{
203    m_pendingEditorCommands.clear();
204
205#ifdef GTK_API_VERSION_2
206    gtk_bindings_activate_event(GTK_OBJECT(m_nativeWidget.get()), event);
207#else
208    gtk_bindings_activate_event(G_OBJECT(m_nativeWidget.get()), event);
209#endif
210
211    if (!m_pendingEditorCommands.isEmpty()) {
212        commandList.appendVector(m_pendingEditorCommands);
213        return;
214    }
215
216    DEPRECATED_DEFINE_STATIC_LOCAL(IntConstCharHashMap, keyDownCommandsMap, ());
217    DEPRECATED_DEFINE_STATIC_LOCAL(IntConstCharHashMap, keyPressCommandsMap, ());
218
219    if (keyDownCommandsMap.isEmpty()) {
220        for (unsigned i = 0; i < G_N_ELEMENTS(keyDownEntries); i++)
221            keyDownCommandsMap.set(keyDownEntries[i].state << 16 | keyDownEntries[i].gdkKeyCode, keyDownEntries[i].name);
222
223        for (unsigned i = 0; i < G_N_ELEMENTS(keyPressEntries); i++)
224            keyPressCommandsMap.set(keyPressEntries[i].state << 16 | keyPressEntries[i].gdkKeyCode, keyPressEntries[i].name);
225    }
226
227    // Special-case enter keys for we want them to work regardless of modifier.
228    if ((event->keyval == GDK_Return || event->keyval == GDK_KP_Enter || event->keyval == GDK_ISO_Enter) && type == KeyPress) {
229        commandList.append("InsertNewLine");
230        return;
231    }
232
233    // For keypress events, we want charCode(), but keyCode() does that.
234    int mapKey = event->state << 16 | event->keyval;
235    if (mapKey) {
236        HashMap<int, const char*>* commandMap = type == KeyDown ?  &keyDownCommandsMap : &keyPressCommandsMap;
237        if (const char* commandString = commandMap->get(mapKey)) {
238            commandList.append(commandString);
239            return;
240        }
241    }
242}
243
244} // namespace WebCore
245