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