Bug 1855360 - Fix the skip-if syntax. a=bustage-fix
[gecko.git] / widget / gtk / NativeKeyBindings.cpp
blobe050fd07c023923c98420f3c9c77681299a02d81
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/ArrayUtils.h"
7 #include "mozilla/MathAlgorithms.h"
8 #include "mozilla/Maybe.h"
9 #include "mozilla/NativeKeyBindingsType.h"
10 #include "mozilla/StaticPrefs_ui.h"
11 #include "mozilla/TextEvents.h"
12 #include "mozilla/WritingModes.h"
14 #include "NativeKeyBindings.h"
15 #include "nsString.h"
16 #include "nsGtkKeyUtils.h"
18 #include <gtk/gtk.h>
19 #include <gdk/gdkkeysyms.h>
20 #include <gdk/gdkkeysyms-compat.h>
21 #include <gdk/gdk.h>
23 namespace mozilla {
24 namespace widget {
26 static nsTArray<CommandInt>* gCurrentCommands = nullptr;
27 static bool gHandled = false;
29 inline void AddCommand(Command aCommand) {
30 MOZ_ASSERT(gCurrentCommands);
31 gCurrentCommands->AppendElement(static_cast<CommandInt>(aCommand));
34 // Common GtkEntry and GtkTextView signals
35 static void copy_clipboard_cb(GtkWidget* w, gpointer user_data) {
36 AddCommand(Command::Copy);
37 g_signal_stop_emission_by_name(w, "copy_clipboard");
38 gHandled = true;
41 static void cut_clipboard_cb(GtkWidget* w, gpointer user_data) {
42 AddCommand(Command::Cut);
43 g_signal_stop_emission_by_name(w, "cut_clipboard");
44 gHandled = true;
47 // GTK distinguishes between display lines (wrapped, as they appear on the
48 // screen) and paragraphs, which are runs of text terminated by a newline.
49 // We don't have this distinction, so we always use editor's notion of
50 // lines, which are newline-terminated.
52 static const Command sDeleteCommands[][2] = {
53 // backward, forward
54 // CHARS
55 {Command::DeleteCharBackward, Command::DeleteCharForward},
56 // WORD_ENDS
57 {Command::DeleteWordBackward, Command::DeleteWordForward},
58 // WORDS
59 {Command::DeleteWordBackward, Command::DeleteWordForward},
60 // LINES
61 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
62 // LINE_ENDS
63 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
64 // PARAGRAPH_ENDS
65 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
66 // PARAGRAPHS
67 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
68 // This deletes from the end of the previous word to the beginning of the
69 // next word, but only if the caret is not in a word.
70 // XXX need to implement in editor
71 {Command::DoNothing, Command::DoNothing} // WHITESPACE
74 static void delete_from_cursor_cb(GtkWidget* w, GtkDeleteType del_type,
75 gint count, gpointer user_data) {
76 g_signal_stop_emission_by_name(w, "delete_from_cursor");
77 if (count == 0) {
78 // Nothing to do.
79 return;
82 bool forward = count > 0;
84 // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in
85 // 3.18 if the user has custom bindings set. See bug 1176929.
86 if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) &&
87 !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) {
88 GtkStyleContext* context = gtk_widget_get_style_context(w);
89 GtkStateFlags flags = gtk_widget_get_state_flags(w);
91 GPtrArray* array;
92 gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr);
93 if (!array) return;
94 g_ptr_array_unref(array);
97 gHandled = true;
98 if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) {
99 // unsupported deletion type
100 return;
103 if (del_type == GTK_DELETE_WORDS) {
104 // This works like word_ends, except we first move the caret to the
105 // beginning/end of the current word.
106 if (forward) {
107 AddCommand(Command::WordNext);
108 AddCommand(Command::WordPrevious);
109 } else {
110 AddCommand(Command::WordPrevious);
111 AddCommand(Command::WordNext);
113 } else if (del_type == GTK_DELETE_DISPLAY_LINES ||
114 del_type == GTK_DELETE_PARAGRAPHS) {
115 // This works like display_line_ends, except we first move the caret to the
116 // beginning/end of the current line.
117 if (forward) {
118 AddCommand(Command::BeginLine);
119 } else {
120 AddCommand(Command::EndLine);
124 Command command = sDeleteCommands[del_type][forward];
125 if (command == Command::DoNothing) {
126 return;
129 unsigned int absCount = Abs(count);
130 for (unsigned int i = 0; i < absCount; ++i) {
131 AddCommand(command);
135 static const Command sMoveCommands[][2][2] = {
136 // non-extend { backward, forward }, extend { backward, forward }
137 // GTK differentiates between logical position, which is prev/next,
138 // and visual position, which is always left/right.
139 // We should fix this to work the same way for RTL text input.
140 {// LOGICAL_POSITIONS
141 {Command::CharPrevious, Command::CharNext},
142 {Command::SelectCharPrevious, Command::SelectCharNext}},
143 {// VISUAL_POSITIONS
144 {Command::CharPrevious, Command::CharNext},
145 {Command::SelectCharPrevious, Command::SelectCharNext}},
146 {// WORDS
147 {Command::WordPrevious, Command::WordNext},
148 {Command::SelectWordPrevious, Command::SelectWordNext}},
149 {// DISPLAY_LINES
150 {Command::LinePrevious, Command::LineNext},
151 {Command::SelectLinePrevious, Command::SelectLineNext}},
152 {// DISPLAY_LINE_ENDS
153 {Command::BeginLine, Command::EndLine},
154 {Command::SelectBeginLine, Command::SelectEndLine}},
155 {// PARAGRAPHS
156 {Command::LinePrevious, Command::LineNext},
157 {Command::SelectLinePrevious, Command::SelectLineNext}},
158 {// PARAGRAPH_ENDS
159 {Command::BeginLine, Command::EndLine},
160 {Command::SelectBeginLine, Command::SelectEndLine}},
161 {// PAGES
162 {Command::MovePageUp, Command::MovePageDown},
163 {Command::SelectPageUp, Command::SelectPageDown}},
164 {// BUFFER_ENDS
165 {Command::MoveTop, Command::MoveBottom},
166 {Command::SelectTop, Command::SelectBottom}},
167 {// HORIZONTAL_PAGES (unsupported)
168 {Command::DoNothing, Command::DoNothing},
169 {Command::DoNothing, Command::DoNothing}}};
171 static void move_cursor_cb(GtkWidget* w, GtkMovementStep step, gint count,
172 gboolean extend_selection, gpointer user_data) {
173 g_signal_stop_emission_by_name(w, "move_cursor");
174 if (count == 0) {
175 // Nothing to do.
176 return;
179 gHandled = true;
180 bool forward = count > 0;
181 if (uint32_t(step) >= ArrayLength(sMoveCommands)) {
182 // unsupported movement type
183 return;
186 Command command = sMoveCommands[step][extend_selection][forward];
187 if (command == Command::DoNothing) {
188 return;
191 unsigned int absCount = Abs(count);
192 for (unsigned int i = 0; i < absCount; ++i) {
193 AddCommand(command);
197 static void paste_clipboard_cb(GtkWidget* w, gpointer user_data) {
198 AddCommand(Command::Paste);
199 g_signal_stop_emission_by_name(w, "paste_clipboard");
200 gHandled = true;
203 // GtkTextView-only signals
204 static void select_all_cb(GtkWidget* aWidget, gboolean aSelect,
205 gpointer aUserData) {
206 // We don't support "Unselect All" command.
207 // Note that if we'd support it, `Ctrl-Shift-a` will be mapped to it and
208 // overrides open `about:addons` shortcut.
209 if (aSelect) {
210 AddCommand(Command::SelectAll);
212 g_signal_stop_emission_by_name(aWidget, "select_all");
213 // Although we prevent the default of `GtkTExtView` with
214 // `g_signal_stop_emission_by_name`, but `gHandled` is used for asserting
215 // if it does not match with the emptiness of the command array.
216 // Therefore, we should not set it to `true` if we don't add a command.
217 gHandled |= aSelect;
220 NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
221 NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
223 // static
224 NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) {
225 switch (aType) {
226 case NativeKeyBindingsType::SingleLineEditor:
227 if (!sInstanceForSingleLineEditor) {
228 sInstanceForSingleLineEditor = new NativeKeyBindings();
229 sInstanceForSingleLineEditor->Init(aType);
231 return sInstanceForSingleLineEditor;
233 default:
234 // fallback to multiline editor case in release build
235 MOZ_FALLTHROUGH_ASSERT("aType is invalid or not yet implemented");
236 case NativeKeyBindingsType::MultiLineEditor:
237 case NativeKeyBindingsType::RichTextEditor:
238 if (!sInstanceForMultiLineEditor) {
239 sInstanceForMultiLineEditor = new NativeKeyBindings();
240 sInstanceForMultiLineEditor->Init(aType);
242 return sInstanceForMultiLineEditor;
246 // static
247 void NativeKeyBindings::Shutdown() {
248 delete sInstanceForSingleLineEditor;
249 sInstanceForSingleLineEditor = nullptr;
250 delete sInstanceForMultiLineEditor;
251 sInstanceForMultiLineEditor = nullptr;
254 void NativeKeyBindings::Init(NativeKeyBindingsType aType) {
255 switch (aType) {
256 case NativeKeyBindingsType::SingleLineEditor:
257 mNativeTarget = gtk_entry_new();
258 break;
259 default:
260 mNativeTarget = gtk_text_view_new();
261 g_signal_connect(mNativeTarget, "select_all", G_CALLBACK(select_all_cb),
262 this);
263 break;
266 g_object_ref_sink(mNativeTarget);
268 g_signal_connect(mNativeTarget, "copy_clipboard",
269 G_CALLBACK(copy_clipboard_cb), this);
270 g_signal_connect(mNativeTarget, "cut_clipboard", G_CALLBACK(cut_clipboard_cb),
271 this);
272 g_signal_connect(mNativeTarget, "delete_from_cursor",
273 G_CALLBACK(delete_from_cursor_cb), this);
274 g_signal_connect(mNativeTarget, "move_cursor", G_CALLBACK(move_cursor_cb),
275 this);
276 g_signal_connect(mNativeTarget, "paste_clipboard",
277 G_CALLBACK(paste_clipboard_cb), this);
280 NativeKeyBindings::~NativeKeyBindings() {
281 gtk_widget_destroy(mNativeTarget);
282 g_object_unref(mNativeTarget);
285 void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
286 const Maybe<WritingMode>& aWritingMode,
287 nsTArray<CommandInt>& aCommands) {
288 MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
289 MOZ_ASSERT(aCommands.IsEmpty());
291 // It must be a DOM event dispached by chrome script.
292 if (!aEvent.mNativeKeyEvent) {
293 return;
296 guint keyval;
297 if (aEvent.mCharCode) {
298 keyval = gdk_unicode_to_keyval(aEvent.mCharCode);
299 } else if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
300 aWritingMode.ref().IsVertical()) {
301 // TODO: Use KeyNameIndex rather than legacy keyCode.
302 uint32_t remappedGeckoKeyCode =
303 aEvent.GetRemappedKeyCode(aWritingMode.ref());
304 switch (remappedGeckoKeyCode) {
305 case NS_VK_UP:
306 keyval = GDK_Up;
307 break;
308 case NS_VK_DOWN:
309 keyval = GDK_Down;
310 break;
311 case NS_VK_LEFT:
312 keyval = GDK_Left;
313 break;
314 case NS_VK_RIGHT:
315 keyval = GDK_Right;
316 break;
317 default:
318 MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
319 return;
321 } else {
322 keyval = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval;
325 if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
326 return;
329 for (uint32_t i = 0; i < aEvent.mAlternativeCharCodes.Length(); ++i) {
330 uint32_t ch = aEvent.IsShift()
331 ? aEvent.mAlternativeCharCodes[i].mShiftedCharCode
332 : aEvent.mAlternativeCharCodes[i].mUnshiftedCharCode;
333 if (ch && ch != aEvent.mCharCode) {
334 keyval = gdk_unicode_to_keyval(ch);
335 if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
336 return;
341 // If the key event does not cause any commands, and we're for single line
342 // editor, let's check whether the key combination is for "select-all" in
343 // GtkTextView because the signal is not supported by GtkEntry.
344 if (aCommands.IsEmpty() && this == sInstanceForSingleLineEditor &&
345 StaticPrefs::ui_key_use_select_all_in_single_line_editor()) {
346 if (NativeKeyBindings* bindingsForMultilineEditor =
347 GetInstance(NativeKeyBindingsType::MultiLineEditor)) {
348 bindingsForMultilineEditor->GetEditCommands(aEvent, aWritingMode,
349 aCommands);
350 if (aCommands.Length() == 1u &&
351 aCommands[0u] == static_cast<CommandInt>(Command::SelectAll)) {
352 return;
354 aCommands.Clear();
359 gtk_bindings_activate_event is preferable, but it has unresolved bug:
360 http://bugzilla.gnome.org/show_bug.cgi?id=162726
361 The bug was already marked as FIXED. However, somebody reports that the
362 bug still exists.
363 Also gtk_bindings_activate may work with some non-shortcuts operations
364 (todo: check it). See bug 411005 and bug 406407.
366 Code, which should be used after fixing GNOME bug 162726:
368 gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget),
369 static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent));
373 bool NativeKeyBindings::GetEditCommandsInternal(
374 const WidgetKeyboardEvent& aEvent, nsTArray<CommandInt>& aCommands,
375 guint aKeyval) {
376 guint modifiers = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->state;
378 gCurrentCommands = &aCommands;
380 gHandled = false;
381 gtk_bindings_activate(G_OBJECT(mNativeTarget), aKeyval,
382 GdkModifierType(modifiers));
384 gCurrentCommands = nullptr;
386 return gHandled;
389 // static
390 void NativeKeyBindings::GetEditCommandsForTests(
391 NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
392 const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
393 MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
395 if (aEvent.IsAlt() || aEvent.IsMeta()) {
396 return;
399 static const size_t kBackward = 0;
400 static const size_t kForward = 1;
401 const size_t extentSelection = aEvent.IsShift() ? 1 : 0;
402 // https://github.com/GNOME/gtk/blob/1f141c19533f4b3f397c3959ade673ce243b6138/gtk/gtktext.c#L1289
403 // https://github.com/GNOME/gtk/blob/c5dd34344f0c660ceffffb3bf9da43c263db16e1/gtk/gtktextview.c#L1534
404 Command command = Command::DoNothing;
405 const KeyNameIndex remappedKeyNameIndex =
406 aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
407 : aEvent.mKeyNameIndex;
408 switch (remappedKeyNameIndex) {
409 case KEY_NAME_INDEX_USE_STRING:
410 switch (aEvent.PseudoCharCode()) {
411 case 'a':
412 case 'A':
413 if (aEvent.IsControl()) {
414 command = Command::SelectAll;
416 break;
417 case 'c':
418 case 'C':
419 if (aEvent.IsControl() && !aEvent.IsShift()) {
420 command = Command::Copy;
422 break;
423 case 'u':
424 case 'U':
425 if (aType == NativeKeyBindingsType::SingleLineEditor &&
426 aEvent.IsControl() && !aEvent.IsShift()) {
427 command = sDeleteCommands[GTK_DELETE_PARAGRAPH_ENDS][kBackward];
429 break;
430 case 'v':
431 case 'V':
432 if (aEvent.IsControl() && !aEvent.IsShift()) {
433 command = Command::Paste;
435 break;
436 case 'x':
437 case 'X':
438 if (aEvent.IsControl() && !aEvent.IsShift()) {
439 command = Command::Cut;
441 break;
442 case '/':
443 if (aEvent.IsControl() && !aEvent.IsShift()) {
444 command = Command::SelectAll;
446 break;
447 default:
448 break;
450 break;
451 case KEY_NAME_INDEX_Insert:
452 if (aEvent.IsControl() && !aEvent.IsShift()) {
453 command = Command::Copy;
454 } else if (aEvent.IsShift() && !aEvent.IsControl()) {
455 command = Command::Paste;
457 break;
458 case KEY_NAME_INDEX_Delete:
459 if (aEvent.IsShift()) {
460 command = Command::Cut;
461 break;
463 [[fallthrough]];
464 case KEY_NAME_INDEX_Backspace: {
465 const size_t direction =
466 remappedKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward;
467 const GtkDeleteType amount =
468 aEvent.IsControl() && aEvent.IsShift()
469 ? GTK_DELETE_PARAGRAPH_ENDS
470 // FYI: Shift key for Backspace is ignored to help mis-typing.
471 : (aEvent.IsControl() ? GTK_DELETE_WORD_ENDS : GTK_DELETE_CHARS);
472 command = sDeleteCommands[amount][direction];
473 break;
475 case KEY_NAME_INDEX_ArrowLeft:
476 case KEY_NAME_INDEX_ArrowRight: {
477 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowRight
478 ? kForward
479 : kBackward;
480 const GtkMovementStep amount = aEvent.IsControl()
481 ? GTK_MOVEMENT_WORDS
482 : GTK_MOVEMENT_VISUAL_POSITIONS;
483 command = sMoveCommands[amount][extentSelection][direction];
484 break;
486 case KEY_NAME_INDEX_ArrowUp:
487 case KEY_NAME_INDEX_ArrowDown: {
488 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowDown
489 ? kForward
490 : kBackward;
491 const GtkMovementStep amount = aEvent.IsControl()
492 ? GTK_MOVEMENT_PARAGRAPHS
493 : GTK_MOVEMENT_DISPLAY_LINES;
494 command = sMoveCommands[amount][extentSelection][direction];
495 break;
497 case KEY_NAME_INDEX_Home:
498 case KEY_NAME_INDEX_End: {
499 const size_t direction =
500 remappedKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward;
501 const GtkMovementStep amount = aEvent.IsControl()
502 ? GTK_MOVEMENT_BUFFER_ENDS
503 : GTK_MOVEMENT_DISPLAY_LINE_ENDS;
504 command = sMoveCommands[amount][extentSelection][direction];
505 break;
507 case KEY_NAME_INDEX_PageUp:
508 case KEY_NAME_INDEX_PageDown: {
509 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_PageDown
510 ? kForward
511 : kBackward;
512 const GtkMovementStep amount = aEvent.IsControl()
513 ? GTK_MOVEMENT_HORIZONTAL_PAGES
514 : GTK_MOVEMENT_PAGES;
515 command = sMoveCommands[amount][extentSelection][direction];
516 break;
518 default:
519 break;
521 if (command != Command::DoNothing) {
522 aCommands.AppendElement(static_cast<CommandInt>(command));
526 } // namespace widget
527 } // namespace mozilla