/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 |