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"
16 #include "nsGtkKeyUtils.h"
19 #include <gdk/gdkkeysyms.h>
20 #include <gdk/gdkkeysyms-compat.h>
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");
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");
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] = {
55 {Command::DeleteCharBackward
, Command::DeleteCharForward
},
57 {Command::DeleteWordBackward
, Command::DeleteWordForward
},
59 {Command::DeleteWordBackward
, Command::DeleteWordForward
},
61 {Command::DeleteToBeginningOfLine
, Command::DeleteToEndOfLine
},
63 {Command::DeleteToBeginningOfLine
, Command::DeleteToEndOfLine
},
65 {Command::DeleteToBeginningOfLine
, Command::DeleteToEndOfLine
},
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");
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
);
92 gtk_style_context_get(context
, flags
, "gtk-key-bindings", &array
, nullptr);
94 g_ptr_array_unref(array
);
98 if (uint32_t(del_type
) >= std::size(sDeleteCommands
)) {
99 // unsupported deletion type
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.
107 AddCommand(Command::WordNext
);
108 AddCommand(Command::WordPrevious
);
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.
118 AddCommand(Command::BeginLine
);
120 AddCommand(Command::EndLine
);
124 Command command
= sDeleteCommands
[del_type
][forward
];
125 if (command
== Command::DoNothing
) {
129 unsigned int absCount
= Abs(count
);
130 for (unsigned int i
= 0; i
< absCount
; ++i
) {
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
}},
144 {Command::CharPrevious
, Command::CharNext
},
145 {Command::SelectCharPrevious
, Command::SelectCharNext
}},
147 {Command::WordPrevious
, Command::WordNext
},
148 {Command::SelectWordPrevious
, Command::SelectWordNext
}},
150 {Command::LinePrevious
, Command::LineNext
},
151 {Command::SelectLinePrevious
, Command::SelectLineNext
}},
152 {// DISPLAY_LINE_ENDS
153 {Command::BeginLine
, Command::EndLine
},
154 {Command::SelectBeginLine
, Command::SelectEndLine
}},
156 {Command::LinePrevious
, Command::LineNext
},
157 {Command::SelectLinePrevious
, Command::SelectLineNext
}},
159 {Command::BeginLine
, Command::EndLine
},
160 {Command::SelectBeginLine
, Command::SelectEndLine
}},
162 {Command::MovePageUp
, Command::MovePageDown
},
163 {Command::SelectPageUp
, Command::SelectPageDown
}},
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");
180 bool forward
= count
> 0;
181 if (uint32_t(step
) >= std::size(sMoveCommands
)) {
182 // unsupported movement type
186 Command command
= sMoveCommands
[step
][extend_selection
][forward
];
187 if (command
== Command::DoNothing
) {
191 unsigned int absCount
= Abs(count
);
192 for (unsigned int i
= 0; i
< absCount
; ++i
) {
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");
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.
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.
220 NativeKeyBindings
* NativeKeyBindings::sInstanceForSingleLineEditor
= nullptr;
221 NativeKeyBindings
* NativeKeyBindings::sInstanceForMultiLineEditor
= nullptr;
224 NativeKeyBindings
* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType
) {
226 case NativeKeyBindingsType::SingleLineEditor
:
227 if (!sInstanceForSingleLineEditor
) {
228 sInstanceForSingleLineEditor
= new NativeKeyBindings();
229 sInstanceForSingleLineEditor
->Init(aType
);
231 return sInstanceForSingleLineEditor
;
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
;
247 void NativeKeyBindings::Shutdown() {
248 delete sInstanceForSingleLineEditor
;
249 sInstanceForSingleLineEditor
= nullptr;
250 delete sInstanceForMultiLineEditor
;
251 sInstanceForMultiLineEditor
= nullptr;
254 void NativeKeyBindings::Init(NativeKeyBindingsType aType
) {
256 case NativeKeyBindingsType::SingleLineEditor
:
257 mNativeTarget
= gtk_entry_new();
260 mNativeTarget
= gtk_text_view_new();
261 g_signal_connect(mNativeTarget
, "select_all", G_CALLBACK(select_all_cb
),
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
),
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
),
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
) {
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
) {
318 MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
322 keyval
= static_cast<GdkEventKey
*>(aEvent
.mNativeKeyEvent
)->keyval
;
325 if (GetEditCommandsInternal(aEvent
, aCommands
, keyval
)) {
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
)) {
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
,
350 if (aCommands
.Length() == 1u &&
351 aCommands
[0u] == static_cast<CommandInt
>(Command::SelectAll
)) {
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
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
,
376 guint modifiers
= static_cast<GdkEventKey
*>(aEvent
.mNativeKeyEvent
)->state
;
378 gCurrentCommands
= &aCommands
;
381 gtk_bindings_activate(G_OBJECT(mNativeTarget
), aKeyval
,
382 GdkModifierType(modifiers
));
384 gCurrentCommands
= nullptr;
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()) {
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()) {
413 if (aEvent
.IsControl()) {
414 command
= Command::SelectAll
;
419 if (aEvent
.IsControl() && !aEvent
.IsShift()) {
420 command
= Command::Copy
;
425 if (aType
== NativeKeyBindingsType::SingleLineEditor
&&
426 aEvent
.IsControl() && !aEvent
.IsShift()) {
427 command
= sDeleteCommands
[GTK_DELETE_PARAGRAPH_ENDS
][kBackward
];
432 if (aEvent
.IsControl() && !aEvent
.IsShift()) {
433 command
= Command::Paste
;
438 if (aEvent
.IsControl() && !aEvent
.IsShift()) {
439 command
= Command::Cut
;
443 if (aEvent
.IsControl() && !aEvent
.IsShift()) {
444 command
= Command::SelectAll
;
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
;
458 case KEY_NAME_INDEX_Delete
:
459 if (aEvent
.IsShift()) {
460 command
= Command::Cut
;
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
];
475 case KEY_NAME_INDEX_ArrowLeft
:
476 case KEY_NAME_INDEX_ArrowRight
: {
477 const size_t direction
= remappedKeyNameIndex
== KEY_NAME_INDEX_ArrowRight
480 const GtkMovementStep amount
= aEvent
.IsControl()
482 : GTK_MOVEMENT_VISUAL_POSITIONS
;
483 command
= sMoveCommands
[amount
][extentSelection
][direction
];
486 case KEY_NAME_INDEX_ArrowUp
:
487 case KEY_NAME_INDEX_ArrowDown
: {
488 const size_t direction
= remappedKeyNameIndex
== KEY_NAME_INDEX_ArrowDown
491 const GtkMovementStep amount
= aEvent
.IsControl()
492 ? GTK_MOVEMENT_PARAGRAPHS
493 : GTK_MOVEMENT_DISPLAY_LINES
;
494 command
= sMoveCommands
[amount
][extentSelection
][direction
];
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
];
507 case KEY_NAME_INDEX_PageUp
:
508 case KEY_NAME_INDEX_PageDown
: {
509 const size_t direction
= remappedKeyNameIndex
== KEY_NAME_INDEX_PageDown
512 const GtkMovementStep amount
= aEvent
.IsControl()
513 ? GTK_MOVEMENT_HORIZONTAL_PAGES
514 : GTK_MOVEMENT_PAGES
;
515 command
= sMoveCommands
[amount
][extentSelection
][direction
];
521 if (command
!= Command::DoNothing
) {
522 aCommands
.AppendElement(static_cast<CommandInt
>(command
));
526 } // namespace widget
527 } // namespace mozilla