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