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/. */
5
6 #include "mozilla/ArrayUtils.h"
7 #include "mozilla/MathAlgorithms.h"
8 #include "mozilla/Maybe.h"
9 #include "mozilla/TextEvents.h"
10 #include "mozilla/WritingModes.h"
11
12 #include "NativeKeyBindings.h"
13 #include "nsString.h"
14 #include "nsMemory.h"
15 #include "nsGtkKeyUtils.h"
16
17 #include <gtk/gtk.h>
18 #include <gdk/gdkkeysyms.h>
19 #include <gdk/gdkkeysyms-compat.h>
20 #include <gdk/gdk.h>
21
22 namespace mozilla {
23 namespace widget {
24
25 static nsTArray<CommandInt>* gCurrentCommands = nullptr;
26 static bool gHandled = false;
27
AddCommand(Command aCommand)28 inline void AddCommand(Command aCommand) {
29 MOZ_ASSERT(gCurrentCommands);
30 gCurrentCommands->AppendElement(static_cast<CommandInt>(aCommand));
31 }
32
33 // Common GtkEntry and GtkTextView signals
copy_clipboard_cb(GtkWidget * w,gpointer user_data)34 static void copy_clipboard_cb(GtkWidget* w, gpointer user_data) {
35 AddCommand(Command::Copy);
36 g_signal_stop_emission_by_name(w, "copy_clipboard");
37 gHandled = true;
38 }
39
cut_clipboard_cb(GtkWidget * w,gpointer user_data)40 static void cut_clipboard_cb(GtkWidget* w, gpointer user_data) {
41 AddCommand(Command::Cut);
42 g_signal_stop_emission_by_name(w, "cut_clipboard");
43 gHandled = true;
44 }
45
46 // GTK distinguishes between display lines (wrapped, as they appear on the
47 // screen) and paragraphs, which are runs of text terminated by a newline.
48 // We don't have this distinction, so we always use editor's notion of
49 // lines, which are newline-terminated.
50
51 static const Command sDeleteCommands[][2] = {
52 // backward, forward
53 // CHARS
54 {Command::DeleteCharBackward, Command::DeleteCharForward},
55 // WORD_ENDS
56 {Command::DeleteWordBackward, Command::DeleteWordForward},
57 // WORDS
58 {Command::DeleteWordBackward, Command::DeleteWordForward},
59 // LINES
60 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
61 // LINE_ENDS
62 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
63 // PARAGRAPH_ENDS
64 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
65 // PARAGRAPHS
66 {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
67 // This deletes from the end of the previous word to the beginning of the
68 // next word, but only if the caret is not in a word.
69 // XXX need to implement in editor
70 {Command::DoNothing, Command::DoNothing} // WHITESPACE
71 };
72
delete_from_cursor_cb(GtkWidget * w,GtkDeleteType del_type,gint count,gpointer user_data)73 static void delete_from_cursor_cb(GtkWidget* w, GtkDeleteType del_type,
74 gint count, gpointer user_data) {
75 g_signal_stop_emission_by_name(w, "delete_from_cursor");
76 if (count == 0) {
77 // Nothing to do.
78 return;
79 }
80
81 bool forward = count > 0;
82
83 // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in
84 // 3.18 if the user has custom bindings set. See bug 1176929.
85 if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) &&
86 !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) {
87 GtkStyleContext* context = gtk_widget_get_style_context(w);
88 GtkStateFlags flags = gtk_widget_get_state_flags(w);
89
90 GPtrArray* array;
91 gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr);
92 if (!array) return;
93 g_ptr_array_unref(array);
94 }
95
96 gHandled = true;
97 if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) {
98 // unsupported deletion type
99 return;
100 }
101
102 if (del_type == GTK_DELETE_WORDS) {
103 // This works like word_ends, except we first move the caret to the
104 // beginning/end of the current word.
105 if (forward) {
106 AddCommand(Command::WordNext);
107 AddCommand(Command::WordPrevious);
108 } else {
109 AddCommand(Command::WordPrevious);
110 AddCommand(Command::WordNext);
111 }
112 } else if (del_type == GTK_DELETE_DISPLAY_LINES ||
113 del_type == GTK_DELETE_PARAGRAPHS) {
114 // This works like display_line_ends, except we first move the caret to the
115 // beginning/end of the current line.
116 if (forward) {
117 AddCommand(Command::BeginLine);
118 } else {
119 AddCommand(Command::EndLine);
120 }
121 }
122
123 Command command = sDeleteCommands[del_type][forward];
124 if (command == Command::DoNothing) {
125 return;
126 }
127
128 unsigned int absCount = Abs(count);
129 for (unsigned int i = 0; i < absCount; ++i) {
130 AddCommand(command);
131 }
132 }
133
134 static const Command sMoveCommands[][2][2] = {
135 // non-extend { backward, forward }, extend { backward, forward }
136 // GTK differentiates between logical position, which is prev/next,
137 // and visual position, which is always left/right.
138 // We should fix this to work the same way for RTL text input.
139 {// LOGICAL_POSITIONS
140 {Command::CharPrevious, Command::CharNext},
141 {Command::SelectCharPrevious, Command::SelectCharNext}},
142 {// VISUAL_POSITIONS
143 {Command::CharPrevious, Command::CharNext},
144 {Command::SelectCharPrevious, Command::SelectCharNext}},
145 {// WORDS
146 {Command::WordPrevious, Command::WordNext},
147 {Command::SelectWordPrevious, Command::SelectWordNext}},
148 {// DISPLAY_LINES
149 {Command::LinePrevious, Command::LineNext},
150 {Command::SelectLinePrevious, Command::SelectLineNext}},
151 {// DISPLAY_LINE_ENDS
152 {Command::BeginLine, Command::EndLine},
153 {Command::SelectBeginLine, Command::SelectEndLine}},
154 {// PARAGRAPHS
155 {Command::LinePrevious, Command::LineNext},
156 {Command::SelectLinePrevious, Command::SelectLineNext}},
157 {// PARAGRAPH_ENDS
158 {Command::BeginLine, Command::EndLine},
159 {Command::SelectBeginLine, Command::SelectEndLine}},
160 {// PAGES
161 {Command::MovePageUp, Command::MovePageDown},
162 {Command::SelectPageUp, Command::SelectPageDown}},
163 {// BUFFER_ENDS
164 {Command::MoveTop, Command::MoveBottom},
165 {Command::SelectTop, Command::SelectBottom}},
166 {// HORIZONTAL_PAGES (unsupported)
167 {Command::DoNothing, Command::DoNothing},
168 {Command::DoNothing, Command::DoNothing}}};
169
move_cursor_cb(GtkWidget * w,GtkMovementStep step,gint count,gboolean extend_selection,gpointer user_data)170 static void move_cursor_cb(GtkWidget* w, GtkMovementStep step, gint count,
171 gboolean extend_selection, gpointer user_data) {
172 g_signal_stop_emission_by_name(w, "move_cursor");
173 if (count == 0) {
174 // Nothing to do.
175 return;
176 }
177
178 gHandled = true;
179 bool forward = count > 0;
180 if (uint32_t(step) >= ArrayLength(sMoveCommands)) {
181 // unsupported movement type
182 return;
183 }
184
185 Command command = sMoveCommands[step][extend_selection][forward];
186 if (command == Command::DoNothing) {
187 return;
188 }
189
190 unsigned int absCount = Abs(count);
191 for (unsigned int i = 0; i < absCount; ++i) {
192 AddCommand(command);
193 }
194 }
195
paste_clipboard_cb(GtkWidget * w,gpointer user_data)196 static void paste_clipboard_cb(GtkWidget* w, gpointer user_data) {
197 AddCommand(Command::Paste);
198 g_signal_stop_emission_by_name(w, "paste_clipboard");
199 gHandled = true;
200 }
201
202 // GtkTextView-only signals
select_all_cb(GtkWidget * w,gboolean select,gpointer user_data)203 static void select_all_cb(GtkWidget* w, gboolean select, gpointer user_data) {
204 AddCommand(Command::SelectAll);
205 g_signal_stop_emission_by_name(w, "select_all");
206 gHandled = true;
207 }
208
209 NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
210 NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
211
212 // static
GetInstance(NativeKeyBindingsType aType)213 NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) {
214 switch (aType) {
215 case nsIWidget::NativeKeyBindingsForSingleLineEditor:
216 if (!sInstanceForSingleLineEditor) {
217 sInstanceForSingleLineEditor = new NativeKeyBindings();
218 sInstanceForSingleLineEditor->Init(aType);
219 }
220 return sInstanceForSingleLineEditor;
221
222 default:
223 // fallback to multiline editor case in release build
224 MOZ_FALLTHROUGH_ASSERT("aType is invalid or not yet implemented");
225 case nsIWidget::NativeKeyBindingsForMultiLineEditor:
226 case nsIWidget::NativeKeyBindingsForRichTextEditor:
227 if (!sInstanceForMultiLineEditor) {
228 sInstanceForMultiLineEditor = new NativeKeyBindings();
229 sInstanceForMultiLineEditor->Init(aType);
230 }
231 return sInstanceForMultiLineEditor;
232 }
233 }
234
235 // static
Shutdown()236 void NativeKeyBindings::Shutdown() {
237 delete sInstanceForSingleLineEditor;
238 sInstanceForSingleLineEditor = nullptr;
239 delete sInstanceForMultiLineEditor;
240 sInstanceForMultiLineEditor = nullptr;
241 }
242
Init(NativeKeyBindingsType aType)243 void NativeKeyBindings::Init(NativeKeyBindingsType aType) {
244 switch (aType) {
245 case nsIWidget::NativeKeyBindingsForSingleLineEditor:
246 mNativeTarget = gtk_entry_new();
247 break;
248 default:
249 mNativeTarget = gtk_text_view_new();
250 if (gtk_major_version > 2 ||
251 (gtk_major_version == 2 &&
252 (gtk_minor_version > 2 ||
253 (gtk_minor_version == 2 && gtk_micro_version >= 2)))) {
254 // select_all only exists in gtk >= 2.2.2. Prior to that,
255 // ctrl+a is bound to (move to beginning, select to end).
256 g_signal_connect(mNativeTarget, "select_all", G_CALLBACK(select_all_cb),
257 this);
258 }
259 break;
260 }
261
262 g_object_ref_sink(mNativeTarget);
263
264 g_signal_connect(mNativeTarget, "copy_clipboard",
265 G_CALLBACK(copy_clipboard_cb), this);
266 g_signal_connect(mNativeTarget, "cut_clipboard", G_CALLBACK(cut_clipboard_cb),
267 this);
268 g_signal_connect(mNativeTarget, "delete_from_cursor",
269 G_CALLBACK(delete_from_cursor_cb), this);
270 g_signal_connect(mNativeTarget, "move_cursor", G_CALLBACK(move_cursor_cb),
271 this);
272 g_signal_connect(mNativeTarget, "paste_clipboard",
273 G_CALLBACK(paste_clipboard_cb), this);
274 }
275
~NativeKeyBindings()276 NativeKeyBindings::~NativeKeyBindings() {
277 gtk_widget_destroy(mNativeTarget);
278 g_object_unref(mNativeTarget);
279 }
280
GetEditCommands(const WidgetKeyboardEvent & aEvent,const Maybe<WritingMode> & aWritingMode,nsTArray<CommandInt> & aCommands)281 void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
282 const Maybe<WritingMode>& aWritingMode,
283 nsTArray<CommandInt>& aCommands) {
284 MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
285 MOZ_ASSERT(aCommands.IsEmpty());
286
287 // It must be a DOM event dispached by chrome script.
288 if (!aEvent.mNativeKeyEvent) {
289 return;
290 }
291
292 guint keyval;
293 if (aEvent.mCharCode) {
294 keyval = gdk_unicode_to_keyval(aEvent.mCharCode);
295 } else if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
296 aWritingMode.ref().IsVertical()) {
297 // TODO: Use KeyNameIndex rather than legacy keyCode.
298 uint32_t remappedGeckoKeyCode =
299 aEvent.GetRemappedKeyCode(aWritingMode.ref());
300 switch (remappedGeckoKeyCode) {
301 case NS_VK_UP:
302 keyval = GDK_Up;
303 break;
304 case NS_VK_DOWN:
305 keyval = GDK_Down;
306 break;
307 case NS_VK_LEFT:
308 keyval = GDK_Left;
309 break;
310 case NS_VK_RIGHT:
311 keyval = GDK_Right;
312 break;
313 default:
314 MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
315 return;
316 }
317 } else {
318 keyval = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval;
319 }
320
321 if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
322 return;
323 }
324
325 for (uint32_t i = 0; i < aEvent.mAlternativeCharCodes.Length(); ++i) {
326 uint32_t ch = aEvent.IsShift()
327 ? aEvent.mAlternativeCharCodes[i].mShiftedCharCode
328 : aEvent.mAlternativeCharCodes[i].mUnshiftedCharCode;
329 if (ch && ch != aEvent.mCharCode) {
330 keyval = gdk_unicode_to_keyval(ch);
331 if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
332 return;
333 }
334 }
335 }
336
337 /*
338 gtk_bindings_activate_event is preferable, but it has unresolved bug:
339 http://bugzilla.gnome.org/show_bug.cgi?id=162726
340 The bug was already marked as FIXED. However, somebody reports that the
341 bug still exists.
342 Also gtk_bindings_activate may work with some non-shortcuts operations
343 (todo: check it). See bug 411005 and bug 406407.
344
345 Code, which should be used after fixing GNOME bug 162726:
346
347 gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget),
348 static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent));
349 */
350 }
351
GetEditCommandsInternal(const WidgetKeyboardEvent & aEvent,nsTArray<CommandInt> & aCommands,guint aKeyval)352 bool NativeKeyBindings::GetEditCommandsInternal(
353 const WidgetKeyboardEvent& aEvent, nsTArray<CommandInt>& aCommands,
354 guint aKeyval) {
355 guint modifiers = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->state;
356
357 gCurrentCommands = &aCommands;
358
359 gHandled = false;
360 gtk_bindings_activate(G_OBJECT(mNativeTarget), aKeyval,
361 GdkModifierType(modifiers));
362
363 gCurrentCommands = nullptr;
364
365 MOZ_ASSERT(!gHandled || !aCommands.IsEmpty());
366
367 return gHandled;
368 }
369
370 // static
GetEditCommandsForTests(NativeKeyBindingsType aType,const WidgetKeyboardEvent & aEvent,const Maybe<WritingMode> & aWritingMode,nsTArray<CommandInt> & aCommands)371 void NativeKeyBindings::GetEditCommandsForTests(
372 NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
373 const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
374 MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
375
376 if (aEvent.IsAlt() || aEvent.IsMeta() || aEvent.IsOS()) {
377 return;
378 }
379
380 static const size_t kBackward = 0;
381 static const size_t kForward = 1;
382 const size_t extentSelection = aEvent.IsShift() ? 1 : 0;
383 // https://github.com/GNOME/gtk/blob/1f141c19533f4b3f397c3959ade673ce243b6138/gtk/gtktext.c#L1289
384 // https://github.com/GNOME/gtk/blob/c5dd34344f0c660ceffffb3bf9da43c263db16e1/gtk/gtktextview.c#L1534
385 Command command = Command::DoNothing;
386 const KeyNameIndex remappedKeyNameIndex =
387 aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
388 : aEvent.mKeyNameIndex;
389 switch (remappedKeyNameIndex) {
390 case KEY_NAME_INDEX_USE_STRING:
391 switch (aEvent.PseudoCharCode()) {
392 case 'a':
393 case 'A':
394 if (aEvent.IsControl()) {
395 command = Command::SelectAll;
396 }
397 break;
398 case 'c':
399 case 'C':
400 if (aEvent.IsControl() && !aEvent.IsShift()) {
401 command = Command::Copy;
402 }
403 break;
404 case 'u':
405 case 'U':
406 if (aType == nsIWidget::NativeKeyBindingsForSingleLineEditor &&
407 aEvent.IsControl() && !aEvent.IsShift()) {
408 command = sDeleteCommands[GTK_DELETE_PARAGRAPH_ENDS][kBackward];
409 }
410 break;
411 case 'v':
412 case 'V':
413 if (aEvent.IsControl() && !aEvent.IsShift()) {
414 command = Command::Paste;
415 }
416 break;
417 case 'x':
418 case 'X':
419 if (aEvent.IsControl() && !aEvent.IsShift()) {
420 command = Command::Cut;
421 }
422 break;
423 case '/':
424 if (aEvent.IsControl() && !aEvent.IsShift()) {
425 command = Command::SelectAll;
426 }
427 break;
428 default:
429 break;
430 }
431 break;
432 case KEY_NAME_INDEX_Insert:
433 if (aEvent.IsControl() && !aEvent.IsShift()) {
434 command = Command::Copy;
435 } else if (aEvent.IsShift() && !aEvent.IsControl()) {
436 command = Command::Paste;
437 }
438 break;
439 case KEY_NAME_INDEX_Delete:
440 if (aEvent.IsShift()) {
441 command = Command::Cut;
442 break;
443 }
444 [[fallthrough]];
445 case KEY_NAME_INDEX_Backspace: {
446 const size_t direction =
447 remappedKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward;
448 const GtkDeleteType amount =
449 aEvent.IsControl() && aEvent.IsShift()
450 ? GTK_DELETE_PARAGRAPH_ENDS
451 // FYI: Shift key for Backspace is ignored to help mis-typing.
452 : (aEvent.IsControl() ? GTK_DELETE_WORD_ENDS : GTK_DELETE_CHARS);
453 command = sDeleteCommands[amount][direction];
454 break;
455 }
456 case KEY_NAME_INDEX_ArrowLeft:
457 case KEY_NAME_INDEX_ArrowRight: {
458 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowRight
459 ? kForward
460 : kBackward;
461 const GtkMovementStep amount = aEvent.IsControl()
462 ? GTK_MOVEMENT_WORDS
463 : GTK_MOVEMENT_VISUAL_POSITIONS;
464 command = sMoveCommands[amount][extentSelection][direction];
465 break;
466 }
467 case KEY_NAME_INDEX_ArrowUp:
468 case KEY_NAME_INDEX_ArrowDown: {
469 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowDown
470 ? kForward
471 : kBackward;
472 const GtkMovementStep amount = aEvent.IsControl()
473 ? GTK_MOVEMENT_PARAGRAPHS
474 : GTK_MOVEMENT_DISPLAY_LINES;
475 command = sMoveCommands[amount][extentSelection][direction];
476 break;
477 }
478 case KEY_NAME_INDEX_Home:
479 case KEY_NAME_INDEX_End: {
480 const size_t direction =
481 remappedKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward;
482 const GtkMovementStep amount = aEvent.IsControl()
483 ? GTK_MOVEMENT_BUFFER_ENDS
484 : GTK_MOVEMENT_DISPLAY_LINE_ENDS;
485 command = sMoveCommands[amount][extentSelection][direction];
486 break;
487 }
488 case KEY_NAME_INDEX_PageUp:
489 case KEY_NAME_INDEX_PageDown: {
490 const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_PageDown
491 ? kForward
492 : kBackward;
493 const GtkMovementStep amount = aEvent.IsControl()
494 ? GTK_MOVEMENT_HORIZONTAL_PAGES
495 : GTK_MOVEMENT_PAGES;
496 command = sMoveCommands[amount][extentSelection][direction];
497 break;
498 }
499 default:
500 break;
501 }
502 if (command != Command::DoNothing) {
503 aCommands.AppendElement(static_cast<CommandInt>(command));
504 }
505 }
506
507 } // namespace widget
508 } // namespace mozilla
509