blob: 159971acc6f0d88b82a6a1d5acbf26d9d6cb7a64 [file] [log] [blame]
/*
* Copyright (C) 2010, 2011 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "config.h"
#include "KeyBindingTranslator.h"
#include <WebCore/GtkVersioning.h>
#include <gdk/gdkkeysyms.h>
namespace WebKit {
static void backspaceCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "backspace");
translator->addPendingEditorCommand("DeleteBackward");
}
static void selectAllCallback(GtkWidget* widget, gboolean select, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "select-all");
translator->addPendingEditorCommand(select ? "SelectAll" : "Unselect");
}
static void cutClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "cut-clipboard");
translator->addPendingEditorCommand("Cut");
}
static void copyClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "copy-clipboard");
translator->addPendingEditorCommand("Copy");
}
static void pasteClipboardCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "paste-clipboard");
translator->addPendingEditorCommand("Paste");
}
static void toggleOverwriteCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "toggle-overwrite");
translator->addPendingEditorCommand("OverWrite");
}
#if GTK_CHECK_VERSION(3, 24, 0)
static void insertEmojiCallback(GtkWidget* widget, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "insert-emoji");
translator->addPendingEditorCommand("GtkInsertEmoji");
}
#endif
#if !USE(GTK4)
// GTK+ will still send these signals to the web view. So we can safely stop signal
// emission without breaking accessibility.
static void popupMenuCallback(GtkWidget* widget, KeyBindingTranslator*)
{
g_signal_stop_emission_by_name(widget, "popup-menu");
}
static void showHelpCallback(GtkWidget* widget, KeyBindingTranslator*)
{
g_signal_stop_emission_by_name(widget, "show-help");
}
#endif
static const char* const gtkDeleteCommands[][2] = {
{ "DeleteBackward", "DeleteForward" }, // Characters
{ "DeleteWordBackward", "DeleteWordForward" }, // Word ends
{ "DeleteWordBackward", "DeleteWordForward" }, // Words
{ "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Lines
{ "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Line ends
{ "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraph ends
{ "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraphs
{ 0, 0 } // Whitespace (M-\ in Emacs)
};
static void deleteFromCursorCallback(GtkWidget* widget, GtkDeleteType deleteType, gint count, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "delete-from-cursor");
int direction = count > 0 ? 1 : 0;
// Ensuring that deleteType <= G_N_ELEMENTS here results in a compiler warning
// that the condition is always true.
if (deleteType == GTK_DELETE_WORDS) {
if (!direction) {
translator->addPendingEditorCommand("MoveWordForward");
translator->addPendingEditorCommand("MoveWordBackward");
} else {
translator->addPendingEditorCommand("MoveWordBackward");
translator->addPendingEditorCommand("MoveWordForward");
}
} else if (deleteType == GTK_DELETE_DISPLAY_LINES) {
if (!direction)
translator->addPendingEditorCommand("MoveToBeginningOfLine");
else
translator->addPendingEditorCommand("MoveToEndOfLine");
} else if (deleteType == GTK_DELETE_PARAGRAPHS) {
if (!direction)
translator->addPendingEditorCommand("MoveToBeginningOfParagraph");
else
translator->addPendingEditorCommand("MoveToEndOfParagraph");
}
const char* rawCommand = gtkDeleteCommands[deleteType][direction];
if (!rawCommand)
return;
for (int i = 0; i < abs(count); i++)
translator->addPendingEditorCommand(rawCommand);
}
static const char* const gtkMoveCommands[][4] = {
{ "MoveBackward", "MoveForward",
"MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Forward/backward grapheme
{ "MoveLeft", "MoveRight",
"MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Left/right grapheme
{ "MoveWordBackward", "MoveWordForward",
"MoveWordBackwardAndModifySelection", "MoveWordForwardAndModifySelection" }, // Forward/backward word
{ "MoveUp", "MoveDown",
"MoveUpAndModifySelection", "MoveDownAndModifySelection" }, // Up/down line
{ "MoveToBeginningOfLine", "MoveToEndOfLine",
"MoveToBeginningOfLineAndModifySelection", "MoveToEndOfLineAndModifySelection" }, // Up/down line ends
{ 0, 0,
"MoveParagraphBackwardAndModifySelection", "MoveParagraphForwardAndModifySelection" }, // Up/down paragraphs
{ "MoveToBeginningOfParagraph", "MoveToEndOfParagraph",
"MoveToBeginningOfParagraphAndModifySelection", "MoveToEndOfParagraphAndModifySelection" }, // Up/down paragraph ends.
{ "MovePageUp", "MovePageDown",
"MovePageUpAndModifySelection", "MovePageDownAndModifySelection" }, // Up/down page
{ "MoveToBeginningOfDocument", "MoveToEndOfDocument",
"MoveToBeginningOfDocumentAndModifySelection", "MoveToEndOfDocumentAndModifySelection" }, // Begin/end of buffer
{ 0, 0,
0, 0 } // Horizontal page movement
};
static void moveCursorCallback(GtkWidget* widget, GtkMovementStep step, gint count, gboolean extendSelection, KeyBindingTranslator* translator)
{
g_signal_stop_emission_by_name(widget, "move-cursor");
int direction = count > 0 ? 1 : 0;
if (extendSelection)
direction += 2;
if (static_cast<unsigned>(step) >= G_N_ELEMENTS(gtkMoveCommands))
return;
const char* rawCommand = gtkMoveCommands[step][direction];
if (!rawCommand)
return;
for (int i = 0; i < abs(count); i++)
translator->addPendingEditorCommand(rawCommand);
}
KeyBindingTranslator::KeyBindingTranslator()
: m_nativeWidget(gtk_text_view_new())
{
g_signal_connect(m_nativeWidget, "backspace", G_CALLBACK(backspaceCallback), this);
g_signal_connect(m_nativeWidget, "cut-clipboard", G_CALLBACK(cutClipboardCallback), this);
g_signal_connect(m_nativeWidget, "copy-clipboard", G_CALLBACK(copyClipboardCallback), this);
g_signal_connect(m_nativeWidget, "paste-clipboard", G_CALLBACK(pasteClipboardCallback), this);
g_signal_connect(m_nativeWidget, "select-all", G_CALLBACK(selectAllCallback), this);
g_signal_connect(m_nativeWidget, "move-cursor", G_CALLBACK(moveCursorCallback), this);
g_signal_connect(m_nativeWidget, "delete-from-cursor", G_CALLBACK(deleteFromCursorCallback), this);
g_signal_connect(m_nativeWidget, "toggle-overwrite", G_CALLBACK(toggleOverwriteCallback), this);
#if !USE(GTK4)
g_signal_connect(m_nativeWidget, "popup-menu", G_CALLBACK(popupMenuCallback), this);
g_signal_connect(m_nativeWidget, "show-help", G_CALLBACK(showHelpCallback), this);
#endif
#if GTK_CHECK_VERSION(3, 24, 0)
g_signal_connect(m_nativeWidget, "insert-emoji", G_CALLBACK(insertEmojiCallback), this);
#endif
}
KeyBindingTranslator::~KeyBindingTranslator()
{
ASSERT(!m_nativeWidget);
}
struct KeyCombinationEntry {
unsigned gdkKeyCode;
unsigned state;
const char* name;
};
static const KeyCombinationEntry customKeyBindings[] = {
{ GDK_KEY_b, GDK_CONTROL_MASK, "ToggleBold" },
{ GDK_KEY_i, GDK_CONTROL_MASK, "ToggleItalic" },
{ GDK_KEY_Escape, 0, "Cancel" },
{ GDK_KEY_greater, GDK_CONTROL_MASK, "Cancel" },
{ GDK_KEY_Tab, 0, "InsertTab" },
{ GDK_KEY_Tab, GDK_SHIFT_MASK, "InsertBacktab" },
{ GDK_KEY_Return, 0, "InsertNewLine" },
{ GDK_KEY_KP_Enter, 0, "InsertNewLine" },
{ GDK_KEY_ISO_Enter, 0, "InsertNewLine" },
{ GDK_KEY_Return, GDK_SHIFT_MASK, "InsertLineBreak" },
{ GDK_KEY_KP_Enter, GDK_SHIFT_MASK, "InsertLineBreak" },
{ GDK_KEY_ISO_Enter, GDK_SHIFT_MASK, "InsertLineBreak" },
{ GDK_KEY_V, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "PasteAsPlainText" },
};
static Vector<String> handleKeyBindingsForMap(const KeyCombinationEntry* map, unsigned mapSize, unsigned keyval, GdkModifierType state)
{
// For keypress events, we want charCode(), but keyCode() does that.
unsigned mapKey = (state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) << 16 | keyval;
if (!mapKey)
return { };
for (unsigned i = 0; i < mapSize; ++i) {
if (mapKey == (map[i].state << 16 | map[i].gdkKeyCode))
return { map[i].name };
}
return { };
}
static Vector<String> handleCustomKeyBindings(unsigned keyval, GdkModifierType state)
{
return handleKeyBindingsForMap(customKeyBindings, G_N_ELEMENTS(customKeyBindings), keyval, state);
}
#if USE(GTK4)
Vector<String> KeyBindingTranslator::commandsForKeyEvent(GtkEventControllerKey* controller)
{
ASSERT(m_pendingEditorCommands.isEmpty());
gtk_event_controller_key_forward(GTK_EVENT_CONTROLLER_KEY(controller), m_nativeWidget);
if (!m_pendingEditorCommands.isEmpty())
return WTFMove(m_pendingEditorCommands);
auto* event = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(controller));
return handleCustomKeyBindings(gdk_key_event_get_keyval(event), gdk_event_get_modifier_state(event));
}
#else
Vector<String> KeyBindingTranslator::commandsForKeyEvent(GdkEventKey* event)
{
ASSERT(m_pendingEditorCommands.isEmpty());
gtk_bindings_activate_event(G_OBJECT(m_nativeWidget), event);
if (!m_pendingEditorCommands.isEmpty())
return WTFMove(m_pendingEditorCommands);
guint keyval;
gdk_event_get_keyval(reinterpret_cast<GdkEvent*>(event), &keyval);
GdkModifierType state;
gdk_event_get_state(reinterpret_cast<GdkEvent*>(event), &state);
return handleCustomKeyBindings(keyval, state);
}
#endif
static const KeyCombinationEntry predefinedKeyBindings[] = {
{ GDK_KEY_Left, 0, "MoveLeft" },
{ GDK_KEY_KP_Left, 0, "MoveLeft" },
{ GDK_KEY_Left, GDK_SHIFT_MASK, "MoveBackwardAndModifySelection" },
{ GDK_KEY_KP_Left, GDK_SHIFT_MASK, "MoveBackwardAndModifySelection" },
{ GDK_KEY_Left, GDK_CONTROL_MASK, "MoveWordBackward" },
{ GDK_KEY_KP_Left, GDK_CONTROL_MASK, "MoveWordBackward" },
{ GDK_KEY_Left, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveWordBackwardAndModifySelection" },
{ GDK_KEY_KP_Left, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveWordBackwardAndModifySelectio" },
{ GDK_KEY_Right, 0, "MoveRight" },
{ GDK_KEY_KP_Right, 0, "MoveRight" },
{ GDK_KEY_Right, GDK_SHIFT_MASK, "MoveForwardAndModifySelection" },
{ GDK_KEY_KP_Right, GDK_SHIFT_MASK, "MoveForwardAndModifySelection" },
{ GDK_KEY_Right, GDK_CONTROL_MASK, "MoveWordForward" },
{ GDK_KEY_KP_Right, GDK_CONTROL_MASK, "MoveWordForward" },
{ GDK_KEY_Right, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveWordForwardAndModifySelection" },
{ GDK_KEY_KP_Right, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveWordForwardAndModifySelection" },
{ GDK_KEY_Up, 0, "MoveUp" },
{ GDK_KEY_KP_Up, 0, "MoveUp" },
{ GDK_KEY_Up, GDK_SHIFT_MASK, "MoveUpAndModifySelection" },
{ GDK_KEY_KP_Up, GDK_SHIFT_MASK, "MoveUpAndModifySelection" },
{ GDK_KEY_Down, 0, "MoveDown" },
{ GDK_KEY_KP_Down, 0, "MoveDown" },
{ GDK_KEY_Down, GDK_SHIFT_MASK, "MoveDownAndModifySelection" },
{ GDK_KEY_KP_Down, GDK_SHIFT_MASK, "MoveDownAndModifySelection" },
{ GDK_KEY_Home, 0, "MoveToBeginningOfLine" },
{ GDK_KEY_KP_Home, 0, "MoveToBeginningOfLine" },
{ GDK_KEY_Home, GDK_SHIFT_MASK, "MoveToBeginningOfLineAndModifySelection" },
{ GDK_KEY_KP_Home, GDK_SHIFT_MASK, "MoveToBeginningOfLineAndModifySelection" },
{ GDK_KEY_Home, GDK_CONTROL_MASK, "MoveToBeginningOfDocument" },
{ GDK_KEY_KP_Home, GDK_CONTROL_MASK, "MoveToBeginningOfDocument" },
{ GDK_KEY_Home, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveToBeginningOfDocumentAndModifySelection" },
{ GDK_KEY_KP_Home, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveToBeginningOfDocumentAndModifySelection" },
{ GDK_KEY_End, 0, "MoveToEndOfLine" },
{ GDK_KEY_KP_End, 0, "MoveToEndOfLine" },
{ GDK_KEY_End, GDK_SHIFT_MASK, "MoveToEndOfLineAndModifySelection" },
{ GDK_KEY_KP_End, GDK_SHIFT_MASK, "MoveToEndOfLineAndModifySelection" },
{ GDK_KEY_End, GDK_CONTROL_MASK, "MoveToEndOfDocument" },
{ GDK_KEY_KP_End, GDK_CONTROL_MASK, "MoveToEndOfDocument" },
{ GDK_KEY_End, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveToEndOfDocumentAndModifySelection" },
{ GDK_KEY_KP_End, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "MoveToEndOfDocumentAndModifySelection" },
{ GDK_KEY_Page_Up, 0, "MovePageUp" },
{ GDK_KEY_KP_Page_Up, 0, "MovePageUp" },
{ GDK_KEY_Page_Up, GDK_SHIFT_MASK, "MovePageUpAndModifySelection" },
{ GDK_KEY_KP_Page_Up, GDK_SHIFT_MASK, "MovePageUpAndModifySelection" },
{ GDK_KEY_Page_Down, 0, "MovePageDown" },
{ GDK_KEY_KP_Page_Down, 0, "MovePageDown" },
{ GDK_KEY_Page_Down, GDK_SHIFT_MASK, "MovePageDownAndModifySelection" },
{ GDK_KEY_KP_Page_Down, GDK_SHIFT_MASK, "MovePageDownAndModifySelection" },
{ GDK_KEY_Delete, 0, "DeleteForward" },
{ GDK_KEY_KP_Delete, 0, "DeleteForward" },
{ GDK_KEY_Delete, GDK_CONTROL_MASK, "DeleteWordForward" },
{ GDK_KEY_KP_Delete, GDK_CONTROL_MASK, "DeleteWordForward" },
{ GDK_KEY_BackSpace, 0, "DeleteBackward" },
{ GDK_KEY_BackSpace, GDK_SHIFT_MASK, "DeleteBackward" },
{ GDK_KEY_BackSpace, GDK_CONTROL_MASK, "DeleteWordBackward" },
{ GDK_KEY_a, GDK_CONTROL_MASK, "SelectAll" },
{ GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "Unselect" },
{ GDK_KEY_slash, GDK_CONTROL_MASK, "SelectAll" },
{ GDK_KEY_backslash, GDK_CONTROL_MASK, "Unselect" },
{ GDK_KEY_x, GDK_CONTROL_MASK, "Cut" },
{ GDK_KEY_c, GDK_CONTROL_MASK, "Copy" },
{ GDK_KEY_v, GDK_CONTROL_MASK, "Paste" },
{ GDK_KEY_KP_Delete, GDK_SHIFT_MASK, "Cut" },
{ GDK_KEY_KP_Insert, GDK_CONTROL_MASK, "Copy" },
{ GDK_KEY_KP_Insert, GDK_SHIFT_MASK, "Paste" }
};
Vector<String> KeyBindingTranslator::commandsForKeyval(unsigned keyval, unsigned modifiers)
{
auto commands = handleKeyBindingsForMap(predefinedKeyBindings, G_N_ELEMENTS(predefinedKeyBindings), keyval, static_cast<GdkModifierType>(modifiers));
if (!commands.isEmpty())
return commands;
return handleCustomKeyBindings(keyval, static_cast<GdkModifierType>(modifiers));
}
} // namespace WebKit