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