1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/libgtk2ui/gtk2_key_bindings_handler.h"
6
7#include <gdk/gdkkeysyms.h>
8#include <X11/Xlib.h>
9#include <X11/XKBlib.h>
10
11#include <string>
12
13#include "base/logging.h"
14#include "base/strings/string_util.h"
15#include "chrome/browser/ui/libgtk2ui/gtk2_util.h"
16#include "content/public/browser/native_web_keyboard_event.h"
17#include "ui/base/x/x11_util.h"
18#include "ui/events/event.h"
19
20using ui::TextEditCommandAuraLinux;
21
22// TODO(erg): Rewrite the old gtk_key_bindings_handler_unittest.cc and get them
23// in a state that links. This code was adapted from the content layer GTK
24// code, which had some simple unit tests. However, the changes in the public
25// interface basically meant the tests need to be rewritten; this imposes weird
26// linking requirements regarding GTK+ as we don't have a libgtk2ui_unittests
27// yet. http://crbug.com/358297.
28
29namespace libgtk2ui {
30
31Gtk2KeyBindingsHandler::Gtk2KeyBindingsHandler()
32    : fake_window_(gtk_offscreen_window_new()),
33      handler_(CreateNewHandler()),
34      has_xkb_(false) {
35  gtk_container_add(GTK_CONTAINER(fake_window_), handler_.get());
36
37  int opcode, event, error;
38  int major = XkbMajorVersion;
39  int minor = XkbMinorVersion;
40  has_xkb_ = XkbQueryExtension(gfx::GetXDisplay(), &opcode, &event, &error,
41                               &major, &minor);
42}
43
44Gtk2KeyBindingsHandler::~Gtk2KeyBindingsHandler() {
45  handler_.Destroy();
46  gtk_widget_destroy(fake_window_);
47}
48
49bool Gtk2KeyBindingsHandler::MatchEvent(
50    const ui::Event& event,
51    std::vector<TextEditCommandAuraLinux>* edit_commands) {
52  CHECK(event.IsKeyEvent());
53
54  const ui::KeyEvent& key_event = static_cast<const ui::KeyEvent&>(event);
55  if (key_event.is_char() || !key_event.native_event())
56    return false;
57
58  GdkEventKey gdk_event;
59  BuildGdkEventKeyFromXEvent(key_event.native_event(), &gdk_event);
60
61  edit_commands_.clear();
62  // If this key event matches a predefined key binding, corresponding signal
63  // will be emitted.
64  gtk_bindings_activate_event(GTK_OBJECT(handler_.get()), &gdk_event);
65
66  bool matched = !edit_commands_.empty();
67  if (edit_commands)
68    edit_commands->swap(edit_commands_);
69  return matched;
70}
71
72GtkWidget* Gtk2KeyBindingsHandler::CreateNewHandler() {
73  Handler* handler =
74      static_cast<Handler*>(g_object_new(HandlerGetType(), NULL));
75
76  handler->owner = this;
77
78  // We don't need to show the |handler| object on screen, so set its size to
79  // zero.
80  gtk_widget_set_size_request(GTK_WIDGET(handler), 0, 0);
81
82  // Prevents it from handling any events by itself.
83  gtk_widget_set_sensitive(GTK_WIDGET(handler), FALSE);
84  gtk_widget_set_events(GTK_WIDGET(handler), 0);
85  gtk_widget_set_can_focus(GTK_WIDGET(handler), TRUE);
86
87  return GTK_WIDGET(handler);
88}
89
90void Gtk2KeyBindingsHandler::EditCommandMatched(
91    TextEditCommandAuraLinux::CommandId id,
92    const std::string& value,
93    bool extend_selection) {
94  edit_commands_.push_back(TextEditCommandAuraLinux(id,
95                                                    value,
96                                                    extend_selection));
97}
98
99void Gtk2KeyBindingsHandler::BuildGdkEventKeyFromXEvent(
100    const base::NativeEvent& xevent,
101    GdkEventKey* gdk_event) {
102  GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
103  GdkModifierType consumed, state;
104
105  gdk_event->type = xevent->xany.type == KeyPress ?
106                    GDK_KEY_PRESS : GDK_KEY_RELEASE;
107  gdk_event->time = xevent->xkey.time;
108  gdk_event->state = static_cast<GdkModifierType>(xevent->xkey.state);
109  gdk_event->hardware_keycode = xevent->xkey.keycode;
110
111  if (has_xkb_) {
112    gdk_event->group = XkbGroupForCoreState(xevent->xkey.state);
113  } else {
114    // The overwhelming majority of people will be using X servers that support
115    // XKB. GDK has a fallback here that does some complicated stuff to detect
116    // whether a modifier key affects the keybinding, but that should be
117    // extremely rare.
118    NOTIMPLEMENTED();
119    gdk_event->group = 0;
120  }
121
122  gdk_event->keyval = GDK_VoidSymbol;
123  gdk_keymap_translate_keyboard_state(
124      keymap,
125      gdk_event->hardware_keycode,
126      static_cast<GdkModifierType>(gdk_event->state),
127      gdk_event->group,
128      &gdk_event->keyval,
129      NULL, NULL, &consumed);
130
131  state = static_cast<GdkModifierType>(gdk_event->state & ~consumed);
132  gdk_keymap_add_virtual_modifiers(keymap, &state);
133  gdk_event->state |= state;
134}
135
136void Gtk2KeyBindingsHandler::HandlerInit(Handler *self) {
137  self->owner = NULL;
138}
139
140void Gtk2KeyBindingsHandler::HandlerClassInit(HandlerClass *klass) {
141  GtkTextViewClass* text_view_class = GTK_TEXT_VIEW_CLASS(klass);
142  GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
143
144  // Overrides all virtual methods related to editor key bindings.
145  text_view_class->backspace = BackSpace;
146  text_view_class->copy_clipboard = CopyClipboard;
147  text_view_class->cut_clipboard = CutClipboard;
148  text_view_class->delete_from_cursor = DeleteFromCursor;
149  text_view_class->insert_at_cursor = InsertAtCursor;
150  text_view_class->move_cursor = MoveCursor;
151  text_view_class->paste_clipboard = PasteClipboard;
152  text_view_class->set_anchor = SetAnchor;
153  text_view_class->toggle_overwrite = ToggleOverwrite;
154  widget_class->show_help = ShowHelp;
155
156  // "move-focus", "move-viewport", "select-all" and "toggle-cursor-visible"
157  // have no corresponding virtual methods. Since glib 2.18 (gtk 2.14),
158  // g_signal_override_class_handler() is introduced to override a signal
159  // handler.
160  g_signal_override_class_handler("move-focus",
161                                  G_TYPE_FROM_CLASS(klass),
162                                  G_CALLBACK(MoveFocus));
163
164  g_signal_override_class_handler("move-viewport",
165                                  G_TYPE_FROM_CLASS(klass),
166                                  G_CALLBACK(MoveViewport));
167
168  g_signal_override_class_handler("select-all",
169                                  G_TYPE_FROM_CLASS(klass),
170                                  G_CALLBACK(SelectAll));
171
172  g_signal_override_class_handler("toggle-cursor-visible",
173                                  G_TYPE_FROM_CLASS(klass),
174                                  G_CALLBACK(ToggleCursorVisible));
175}
176
177GType Gtk2KeyBindingsHandler::HandlerGetType() {
178  static volatile gsize type_id_volatile = 0;
179  if (g_once_init_enter(&type_id_volatile)) {
180    GType type_id = g_type_register_static_simple(
181        GTK_TYPE_TEXT_VIEW,
182        g_intern_static_string("Gtk2KeyBindingsHandler"),
183        sizeof(HandlerClass),
184        reinterpret_cast<GClassInitFunc>(HandlerClassInit),
185        sizeof(Handler),
186        reinterpret_cast<GInstanceInitFunc>(HandlerInit),
187        static_cast<GTypeFlags>(0));
188    g_once_init_leave(&type_id_volatile, type_id);
189  }
190  return type_id_volatile;
191}
192
193Gtk2KeyBindingsHandler* Gtk2KeyBindingsHandler::GetHandlerOwner(
194    GtkTextView* text_view) {
195  Handler* handler = G_TYPE_CHECK_INSTANCE_CAST(
196      text_view, HandlerGetType(), Handler);
197  DCHECK(handler);
198  return handler->owner;
199}
200
201void Gtk2KeyBindingsHandler::BackSpace(GtkTextView* text_view) {
202  GetHandlerOwner(text_view)
203      ->EditCommandMatched(
204          TextEditCommandAuraLinux::DELETE_BACKWARD, std::string(), false);
205}
206
207void Gtk2KeyBindingsHandler::CopyClipboard(GtkTextView* text_view) {
208  GetHandlerOwner(text_view)->EditCommandMatched(
209      TextEditCommandAuraLinux::COPY, std::string(), false);
210}
211
212void Gtk2KeyBindingsHandler::CutClipboard(GtkTextView* text_view) {
213  GetHandlerOwner(text_view)->EditCommandMatched(
214      TextEditCommandAuraLinux::CUT, std::string(), false);
215}
216
217void Gtk2KeyBindingsHandler::DeleteFromCursor(
218    GtkTextView* text_view, GtkDeleteType type, gint count) {
219  if (!count)
220    return;
221
222  TextEditCommandAuraLinux::CommandId commands[2] = {
223    TextEditCommandAuraLinux::INVALID_COMMAND,
224    TextEditCommandAuraLinux::INVALID_COMMAND,
225  };
226  switch (type) {
227    case GTK_DELETE_CHARS:
228      commands[0] = (count > 0 ?
229          TextEditCommandAuraLinux::DELETE_FORWARD :
230          TextEditCommandAuraLinux::DELETE_BACKWARD);
231      break;
232    case GTK_DELETE_WORD_ENDS:
233      commands[0] = (count > 0 ?
234          TextEditCommandAuraLinux::DELETE_WORD_FORWARD :
235          TextEditCommandAuraLinux::DELETE_WORD_BACKWARD);
236      break;
237    case GTK_DELETE_WORDS:
238      if (count > 0) {
239        commands[0] = TextEditCommandAuraLinux::MOVE_WORD_FORWARD;
240        commands[1] = TextEditCommandAuraLinux::DELETE_WORD_BACKWARD;
241      } else {
242        commands[0] = TextEditCommandAuraLinux::MOVE_WORD_BACKWARD;
243        commands[1] = TextEditCommandAuraLinux::DELETE_WORD_FORWARD;
244      }
245      break;
246    case GTK_DELETE_DISPLAY_LINES:
247      commands[0] = TextEditCommandAuraLinux::MOVE_TO_BEGINING_OF_LINE;
248      commands[1] = TextEditCommandAuraLinux::DELETE_TO_END_OF_LINE;
249      break;
250    case GTK_DELETE_DISPLAY_LINE_ENDS:
251      commands[0] = (count > 0 ?
252          TextEditCommandAuraLinux::DELETE_TO_END_OF_LINE :
253          TextEditCommandAuraLinux::DELETE_TO_BEGINING_OF_LINE);
254      break;
255    case GTK_DELETE_PARAGRAPH_ENDS:
256      commands[0] = (count > 0 ?
257          TextEditCommandAuraLinux::DELETE_TO_END_OF_PARAGRAPH :
258          TextEditCommandAuraLinux::DELETE_TO_BEGINING_OF_PARAGRAPH);
259      break;
260    case GTK_DELETE_PARAGRAPHS:
261      commands[0] =
262          TextEditCommandAuraLinux::MOVE_TO_BEGINING_OF_PARAGRAPH;
263      commands[1] =
264          TextEditCommandAuraLinux::DELETE_TO_END_OF_PARAGRAPH;
265      break;
266    default:
267      // GTK_DELETE_WHITESPACE has no corresponding editor command.
268      return;
269  }
270
271  Gtk2KeyBindingsHandler* owner = GetHandlerOwner(text_view);
272  if (count < 0)
273    count = -count;
274  for (; count > 0; --count) {
275    for (size_t i = 0; i < arraysize(commands); ++i)
276      if (commands[i] != TextEditCommandAuraLinux::INVALID_COMMAND)
277        owner->EditCommandMatched(commands[i], std::string(), false);
278  }
279}
280
281void Gtk2KeyBindingsHandler::InsertAtCursor(GtkTextView* text_view,
282                                            const gchar* str) {
283  if (str && *str)
284    GetHandlerOwner(text_view)->EditCommandMatched(
285        TextEditCommandAuraLinux::INSERT_TEXT, str, false);
286}
287
288void Gtk2KeyBindingsHandler::MoveCursor(
289    GtkTextView* text_view, GtkMovementStep step, gint count,
290    gboolean extend_selection) {
291  if (!count)
292    return;
293
294  TextEditCommandAuraLinux::CommandId command;
295  switch (step) {
296    case GTK_MOVEMENT_LOGICAL_POSITIONS:
297      command = (count > 0 ?
298                 TextEditCommandAuraLinux::MOVE_FORWARD :
299                 TextEditCommandAuraLinux::MOVE_BACKWARD);
300      break;
301    case GTK_MOVEMENT_VISUAL_POSITIONS:
302      command = (count > 0 ?
303                 TextEditCommandAuraLinux::MOVE_RIGHT :
304                 TextEditCommandAuraLinux::MOVE_LEFT);
305      break;
306    case GTK_MOVEMENT_WORDS:
307      command = (count > 0 ?
308                 TextEditCommandAuraLinux::MOVE_WORD_RIGHT :
309                 TextEditCommandAuraLinux::MOVE_WORD_LEFT);
310      break;
311    case GTK_MOVEMENT_DISPLAY_LINES:
312      command = (count > 0 ?
313                 TextEditCommandAuraLinux::MOVE_DOWN :
314                 TextEditCommandAuraLinux::MOVE_UP);
315      break;
316    case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
317      command = (count > 0 ?
318                 TextEditCommandAuraLinux::MOVE_TO_END_OF_LINE :
319                 TextEditCommandAuraLinux::MOVE_TO_BEGINING_OF_LINE);
320      break;
321    case GTK_MOVEMENT_PARAGRAPH_ENDS:
322      command = (count > 0 ?
323                 TextEditCommandAuraLinux::MOVE_TO_END_OF_PARAGRAPH :
324                 TextEditCommandAuraLinux::MOVE_TO_BEGINING_OF_PARAGRAPH);
325      break;
326    case GTK_MOVEMENT_PAGES:
327      command = (count > 0 ? TextEditCommandAuraLinux::MOVE_PAGE_DOWN :
328                 TextEditCommandAuraLinux::MOVE_PAGE_UP);
329      break;
330    case GTK_MOVEMENT_BUFFER_ENDS:
331      command = (count > 0 ? TextEditCommandAuraLinux::MOVE_TO_END_OF_DOCUMENT :
332                 TextEditCommandAuraLinux::MOVE_TO_BEGINING_OF_DOCUMENT);
333      break;
334    default:
335      // GTK_MOVEMENT_PARAGRAPHS and GTK_MOVEMENT_HORIZONTAL_PAGES have
336      // no corresponding editor commands.
337      return;
338  }
339
340  Gtk2KeyBindingsHandler* owner = GetHandlerOwner(text_view);
341  if (count < 0)
342    count = -count;
343  for (; count > 0; --count)
344    owner->EditCommandMatched(command, std::string(), extend_selection);
345}
346
347void Gtk2KeyBindingsHandler::MoveViewport(
348    GtkTextView* text_view, GtkScrollStep step, gint count) {
349  // Not supported by webkit.
350}
351
352void Gtk2KeyBindingsHandler::PasteClipboard(GtkTextView* text_view) {
353  GetHandlerOwner(text_view)->EditCommandMatched(
354      TextEditCommandAuraLinux::PASTE, std::string(), false);
355}
356
357void Gtk2KeyBindingsHandler::SelectAll(GtkTextView* text_view,
358                                       gboolean select) {
359  if (select) {
360    GetHandlerOwner(text_view)->EditCommandMatched(
361        TextEditCommandAuraLinux::SELECT_ALL, std::string(), false);
362  } else {
363    GetHandlerOwner(text_view)->EditCommandMatched(
364        TextEditCommandAuraLinux::UNSELECT, std::string(), false);
365  }
366}
367
368void Gtk2KeyBindingsHandler::SetAnchor(GtkTextView* text_view) {
369  GetHandlerOwner(text_view)->EditCommandMatched(
370      TextEditCommandAuraLinux::SET_MARK, std::string(), false);
371}
372
373void Gtk2KeyBindingsHandler::ToggleCursorVisible(GtkTextView* text_view) {
374  // Not supported by webkit.
375}
376
377void Gtk2KeyBindingsHandler::ToggleOverwrite(GtkTextView* text_view) {
378  // Not supported by webkit.
379}
380
381gboolean Gtk2KeyBindingsHandler::ShowHelp(GtkWidget* widget,
382                                          GtkWidgetHelpType arg1) {
383  // Just for disabling the default handler.
384  return FALSE;
385}
386
387void Gtk2KeyBindingsHandler::MoveFocus(GtkWidget* widget,
388                                       GtkDirectionType arg1) {
389  // Just for disabling the default handler.
390}
391
392}  // namespace libgtk2ui
393