Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/widget/gtk/NativeKeyBindings.cpp
Line
Count
Source (jump to first uncovered line)
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/TextEvents.h"
9
10
#include "NativeKeyBindings.h"
11
#include "nsString.h"
12
#include "nsMemory.h"
13
#include "nsGtkKeyUtils.h"
14
15
#include <gtk/gtk.h>
16
#include <gdk/gdkkeysyms.h>
17
#include <gdk/gdk.h>
18
19
namespace mozilla {
20
namespace widget {
21
22
static nsTArray<CommandInt>* gCurrentCommands = nullptr;
23
static bool gHandled = false;
24
25
// Common GtkEntry and GtkTextView signals
26
static void
27
copy_clipboard_cb(GtkWidget *w, gpointer user_data)
28
0
{
29
0
  gCurrentCommands->AppendElement(CommandCopy);
30
0
  g_signal_stop_emission_by_name(w, "copy_clipboard");
31
0
  gHandled = true;
32
0
}
33
34
static void
35
cut_clipboard_cb(GtkWidget *w, gpointer user_data)
36
0
{
37
0
  gCurrentCommands->AppendElement(CommandCut);
38
0
  g_signal_stop_emission_by_name(w, "cut_clipboard");
39
0
  gHandled = true;
40
0
}
41
42
// GTK distinguishes between display lines (wrapped, as they appear on the
43
// screen) and paragraphs, which are runs of text terminated by a newline.
44
// We don't have this distinction, so we always use editor's notion of
45
// lines, which are newline-terminated.
46
47
static const Command sDeleteCommands[][2] = {
48
  // backward, forward
49
  { CommandDeleteCharBackward, CommandDeleteCharForward },    // CHARS
50
  { CommandDeleteWordBackward, CommandDeleteWordForward },    // WORD_ENDS
51
  { CommandDeleteWordBackward, CommandDeleteWordForward },    // WORDS
52
  { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // LINES
53
  { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // LINE_ENDS
54
  { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // PARAGRAPH_ENDS
55
  { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // PARAGRAPHS
56
  // This deletes from the end of the previous word to the beginning of the
57
  // next word, but only if the caret is not in a word.
58
  // XXX need to implement in editor
59
  { CommandDoNothing, CommandDoNothing } // WHITESPACE
60
};
61
62
static void
63
delete_from_cursor_cb(GtkWidget *w, GtkDeleteType del_type,
64
                      gint count, gpointer user_data)
65
0
{
66
0
  g_signal_stop_emission_by_name(w, "delete_from_cursor");
67
0
  if (count == 0) {
68
0
    // Nothing to do.
69
0
    return;
70
0
  }
71
0
72
0
  bool forward = count > 0;
73
0
74
0
#ifdef MOZ_WIDGET_GTK
75
0
  // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in
76
0
  // 3.18 if the user has custom bindings set. See bug 1176929.
77
0
  if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) &&
78
0
      !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) {
79
0
    GtkStyleContext* context = gtk_widget_get_style_context(w);
80
0
    GtkStateFlags flags = gtk_widget_get_state_flags(w);
81
0
82
0
    GPtrArray* array;
83
0
    gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr);
84
0
    if (!array)
85
0
      return;
86
0
    g_ptr_array_unref(array);
87
0
  }
88
0
#endif
89
0
90
0
  gHandled = true;
91
0
  if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) {
92
0
    // unsupported deletion type
93
0
    return;
94
0
  }
95
0
96
0
  if (del_type == GTK_DELETE_WORDS) {
97
0
    // This works like word_ends, except we first move the caret to the
98
0
    // beginning/end of the current word.
99
0
    if (forward) {
100
0
      gCurrentCommands->AppendElement(CommandWordNext);
101
0
      gCurrentCommands->AppendElement(CommandWordPrevious);
102
0
    } else {
103
0
      gCurrentCommands->AppendElement(CommandWordPrevious);
104
0
      gCurrentCommands->AppendElement(CommandWordNext);
105
0
    }
106
0
  } else if (del_type == GTK_DELETE_DISPLAY_LINES ||
107
0
             del_type == GTK_DELETE_PARAGRAPHS) {
108
0
109
0
    // This works like display_line_ends, except we first move the caret to the
110
0
    // beginning/end of the current line.
111
0
    if (forward) {
112
0
      gCurrentCommands->AppendElement(CommandBeginLine);
113
0
    } else {
114
0
      gCurrentCommands->AppendElement(CommandEndLine);
115
0
    }
116
0
  }
117
0
118
0
  Command command = sDeleteCommands[del_type][forward];
119
0
  if (!command) {
120
0
    return; // unsupported command
121
0
  }
122
0
123
0
  unsigned int absCount = Abs(count);
124
0
  for (unsigned int i = 0; i < absCount; ++i) {
125
0
    gCurrentCommands->AppendElement(command);
126
0
  }
127
0
}
128
129
static const Command sMoveCommands[][2][2] = {
130
  // non-extend { backward, forward }, extend { backward, forward }
131
  // GTK differentiates between logical position, which is prev/next,
132
  // and visual position, which is always left/right.
133
  // We should fix this to work the same way for RTL text input.
134
  { // LOGICAL_POSITIONS
135
    { CommandCharPrevious, CommandCharNext },
136
    { CommandSelectCharPrevious, CommandSelectCharNext }
137
  },
138
  { // VISUAL_POSITIONS
139
    { CommandCharPrevious, CommandCharNext },
140
    { CommandSelectCharPrevious, CommandSelectCharNext }
141
  },
142
  { // WORDS
143
    { CommandWordPrevious, CommandWordNext },
144
    { CommandSelectWordPrevious, CommandSelectWordNext }
145
  },
146
  { // DISPLAY_LINES
147
    { CommandLinePrevious, CommandLineNext },
148
    { CommandSelectLinePrevious, CommandSelectLineNext }
149
  },
150
  { // DISPLAY_LINE_ENDS
151
    { CommandBeginLine, CommandEndLine },
152
    { CommandSelectBeginLine, CommandSelectEndLine }
153
  },
154
  { // PARAGRAPHS
155
    { CommandLinePrevious, CommandLineNext },
156
    { CommandSelectLinePrevious, CommandSelectLineNext }
157
  },
158
  { // PARAGRAPH_ENDS
159
    { CommandBeginLine, CommandEndLine },
160
    { CommandSelectBeginLine, CommandSelectEndLine }
161
  },
162
  { // PAGES
163
    { CommandMovePageUp, CommandMovePageDown },
164
    { CommandSelectPageUp, CommandSelectPageDown }
165
  },
166
  { // BUFFER_ENDS
167
    { CommandMoveTop, CommandMoveBottom },
168
    { CommandSelectTop, CommandSelectBottom }
169
  },
170
  { // HORIZONTAL_PAGES (unsupported)
171
    { CommandDoNothing, CommandDoNothing },
172
    { CommandDoNothing, CommandDoNothing }
173
  }
174
};
175
176
static void
177
move_cursor_cb(GtkWidget *w, GtkMovementStep step, gint count,
178
               gboolean extend_selection, gpointer user_data)
179
0
{
180
0
  g_signal_stop_emission_by_name(w, "move_cursor");
181
0
  if (count == 0) {
182
0
    // Nothing to do.
183
0
    return;
184
0
  }
185
0
186
0
  gHandled = true;
187
0
  bool forward = count > 0;
188
0
  if (uint32_t(step) >= ArrayLength(sMoveCommands)) {
189
0
    // unsupported movement type
190
0
    return;
191
0
  }
192
0
193
0
  Command command = sMoveCommands[step][extend_selection][forward];
194
0
  if (!command) {
195
0
    return; // unsupported command
196
0
  }
197
0
198
0
  unsigned int absCount = Abs(count);
199
0
  for (unsigned int i = 0; i < absCount; ++i) {
200
0
    gCurrentCommands->AppendElement(command);
201
0
  }
202
0
}
203
204
static void
205
paste_clipboard_cb(GtkWidget *w, gpointer user_data)
206
0
{
207
0
  gCurrentCommands->AppendElement(CommandPaste);
208
0
  g_signal_stop_emission_by_name(w, "paste_clipboard");
209
0
  gHandled = true;
210
0
}
211
212
// GtkTextView-only signals
213
static void
214
select_all_cb(GtkWidget *w, gboolean select, gpointer user_data)
215
0
{
216
0
  gCurrentCommands->AppendElement(CommandSelectAll);
217
0
  g_signal_stop_emission_by_name(w, "select_all");
218
0
  gHandled = true;
219
0
}
220
221
NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
222
NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
223
224
// static
225
NativeKeyBindings*
226
NativeKeyBindings::GetInstance(NativeKeyBindingsType aType)
227
0
{
228
0
  switch (aType) {
229
0
    case nsIWidget::NativeKeyBindingsForSingleLineEditor:
230
0
      if (!sInstanceForSingleLineEditor) {
231
0
        sInstanceForSingleLineEditor = new NativeKeyBindings();
232
0
        sInstanceForSingleLineEditor->Init(aType);
233
0
      }
234
0
      return sInstanceForSingleLineEditor;
235
0
236
0
    default:
237
0
      // fallback to multiline editor case in release build
238
0
      MOZ_FALLTHROUGH_ASSERT("aType is invalid or not yet implemented");
239
0
    case nsIWidget::NativeKeyBindingsForMultiLineEditor:
240
0
    case nsIWidget::NativeKeyBindingsForRichTextEditor:
241
0
      if (!sInstanceForMultiLineEditor) {
242
0
        sInstanceForMultiLineEditor = new NativeKeyBindings();
243
0
        sInstanceForMultiLineEditor->Init(aType);
244
0
      }
245
0
      return sInstanceForMultiLineEditor;
246
0
  }
247
0
}
248
249
// static
250
void
251
NativeKeyBindings::Shutdown()
252
0
{
253
0
  delete sInstanceForSingleLineEditor;
254
0
  sInstanceForSingleLineEditor = nullptr;
255
0
  delete sInstanceForMultiLineEditor;
256
0
  sInstanceForMultiLineEditor = nullptr;
257
0
}
258
259
void
260
NativeKeyBindings::Init(NativeKeyBindingsType  aType)
261
0
{
262
0
  switch (aType) {
263
0
  case nsIWidget::NativeKeyBindingsForSingleLineEditor:
264
0
    mNativeTarget = gtk_entry_new();
265
0
    break;
266
0
  default:
267
0
    mNativeTarget = gtk_text_view_new();
268
0
    if (gtk_major_version > 2 ||
269
0
        (gtk_major_version == 2 && (gtk_minor_version > 2 ||
270
0
                                    (gtk_minor_version == 2 &&
271
0
                                     gtk_micro_version >= 2)))) {
272
0
      // select_all only exists in gtk >= 2.2.2.  Prior to that,
273
0
      // ctrl+a is bound to (move to beginning, select to end).
274
0
      g_signal_connect(mNativeTarget, "select_all",
275
0
                       G_CALLBACK(select_all_cb), this);
276
0
    }
277
0
    break;
278
0
  }
279
0
280
0
  g_object_ref_sink(mNativeTarget);
281
0
282
0
  g_signal_connect(mNativeTarget, "copy_clipboard",
283
0
                   G_CALLBACK(copy_clipboard_cb), this);
284
0
  g_signal_connect(mNativeTarget, "cut_clipboard",
285
0
                   G_CALLBACK(cut_clipboard_cb), this);
286
0
  g_signal_connect(mNativeTarget, "delete_from_cursor",
287
0
                   G_CALLBACK(delete_from_cursor_cb), this);
288
0
  g_signal_connect(mNativeTarget, "move_cursor",
289
0
                   G_CALLBACK(move_cursor_cb), this);
290
0
  g_signal_connect(mNativeTarget, "paste_clipboard",
291
0
                   G_CALLBACK(paste_clipboard_cb), this);
292
0
}
293
294
NativeKeyBindings::~NativeKeyBindings()
295
0
{
296
0
  gtk_widget_destroy(mNativeTarget);
297
0
  g_object_unref(mNativeTarget);
298
0
}
299
300
void
301
NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
302
                                   nsTArray<CommandInt>& aCommands)
303
0
{
304
0
  // If the native key event is set, it must be synthesized for tests.
305
0
  // We just ignore such events because this behavior depends on system
306
0
  // settings.
307
0
  if (!aEvent.mNativeKeyEvent) {
308
0
    // It must be synthesized event or dispatched DOM event from chrome.
309
0
    return;
310
0
  }
311
0
312
0
  guint keyval;
313
0
314
0
  if (aEvent.mCharCode) {
315
0
    keyval = gdk_unicode_to_keyval(aEvent.mCharCode);
316
0
  } else {
317
0
    keyval =
318
0
      static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval;
319
0
  }
320
0
321
0
  if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
322
0
    return;
323
0
  }
324
0
325
0
  for (uint32_t i = 0; i < aEvent.mAlternativeCharCodes.Length(); ++i) {
326
0
    uint32_t ch = aEvent.IsShift() ?
327
0
      aEvent.mAlternativeCharCodes[i].mShiftedCharCode :
328
0
      aEvent.mAlternativeCharCodes[i].mUnshiftedCharCode;
329
0
    if (ch && ch != aEvent.mCharCode) {
330
0
      keyval = gdk_unicode_to_keyval(ch);
331
0
      if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
332
0
        return;
333
0
      }
334
0
    }
335
0
  }
336
0
337
0
/*
338
0
gtk_bindings_activate_event is preferable, but it has unresolved bug:
339
0
http://bugzilla.gnome.org/show_bug.cgi?id=162726
340
0
The bug was already marked as FIXED.  However, somebody reports that the
341
0
bug still exists.
342
0
Also gtk_bindings_activate may work with some non-shortcuts operations
343
0
(todo: check it). See bug 411005 and bug 406407.
344
0
345
0
Code, which should be used after fixing GNOME bug 162726:
346
0
347
0
  gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget),
348
0
    static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent));
349
0
*/
350
0
}
351
352
bool
353
NativeKeyBindings::GetEditCommandsInternal(const WidgetKeyboardEvent& aEvent,
354
                                           nsTArray<CommandInt>& aCommands,
355
                                           guint aKeyval)
356
0
{
357
0
  guint modifiers =
358
0
    static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->state;
359
0
360
0
  gCurrentCommands = &aCommands;
361
0
362
0
  gHandled = false;
363
0
  gtk_bindings_activate(G_OBJECT(mNativeTarget),
364
0
                        aKeyval, GdkModifierType(modifiers));
365
0
366
0
  gCurrentCommands = nullptr;
367
0
368
0
  MOZ_ASSERT(!gHandled || !aCommands.IsEmpty());
369
0
370
0
  return gHandled;
371
0
}
372
373
} // namespace widget
374
} // namespace mozilla