- add sources.
[platform/framework/web/crosswalk.git] / src / content / browser / renderer_host / input / web_input_event_builders_gtk.cc
1 // Copyright 2013 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 "content/browser/renderer_host/input/web_input_event_builders_gtk.h"
6
7 #include <gdk/gdk.h>
8 #include <gdk/gdkkeysyms.h>
9 #include <gtk/gtk.h>
10
11 #include "base/logging.h"
12 #include "content/browser/renderer_host/input/web_input_event_util_posix.h"
13 #include "third_party/WebKit/public/web/WebInputEvent.h"
14 #include "ui/events/keycodes/keyboard_code_conversion_gtk.h"
15
16 using WebKit::WebInputEvent;
17 using WebKit::WebMouseEvent;
18 using WebKit::WebMouseWheelEvent;
19 using WebKit::WebKeyboardEvent;
20
21 namespace {
22
23 // For click count tracking.
24 static int num_clicks = 0;
25 static GdkWindow* last_click_event_window = 0;
26 static gint last_click_time = 0;
27 static gint last_click_x = 0;
28 static gint last_click_y = 0;
29 static WebMouseEvent::Button last_click_button = WebMouseEvent::ButtonNone;
30
31 bool ShouldForgetPreviousClick(GdkWindow* window, gint time, gint x, gint y) {
32   static GtkSettings* settings = gtk_settings_get_default();
33
34   if (window != last_click_event_window)
35     return true;
36
37   gint double_click_time = 250;
38   gint double_click_distance = 5;
39   g_object_get(G_OBJECT(settings),
40                "gtk-double-click-time",
41                &double_click_time,
42                "gtk-double-click-distance",
43                &double_click_distance,
44                NULL);
45   return (time - last_click_time) > double_click_time ||
46          std::abs(x - last_click_x) > double_click_distance ||
47          std::abs(y - last_click_y) > double_click_distance;
48 }
49
50 void ResetClickCountState() {
51   num_clicks = 0;
52   last_click_event_window = 0;
53   last_click_time = 0;
54   last_click_x = 0;
55   last_click_y = 0;
56   last_click_button = WebKit::WebMouseEvent::ButtonNone;
57 }
58
59 bool IsKeyPadKeyval(guint keyval) {
60   // Keypad keyvals all fall into one range.
61   return keyval >= GDK_KP_Space && keyval <= GDK_KP_9;
62 }
63
64 double GdkEventTimeToWebEventTime(guint32 time) {
65   // Convert from time in ms to time in sec.
66   return time / 1000.0;
67 }
68
69 int GdkStateToWebEventModifiers(guint state) {
70   int modifiers = 0;
71   if (state & GDK_SHIFT_MASK)
72     modifiers |= WebInputEvent::ShiftKey;
73   if (state & GDK_CONTROL_MASK)
74     modifiers |= WebInputEvent::ControlKey;
75   if (state & GDK_MOD1_MASK)
76     modifiers |= WebInputEvent::AltKey;
77   if (state & GDK_META_MASK)
78     modifiers |= WebInputEvent::MetaKey;
79   if (state & GDK_BUTTON1_MASK)
80     modifiers |= WebInputEvent::LeftButtonDown;
81   if (state & GDK_BUTTON2_MASK)
82     modifiers |= WebInputEvent::MiddleButtonDown;
83   if (state & GDK_BUTTON3_MASK)
84     modifiers |= WebInputEvent::RightButtonDown;
85   if (state & GDK_LOCK_MASK)
86     modifiers |= WebInputEvent::CapsLockOn;
87   if (state & GDK_MOD2_MASK)
88     modifiers |= WebInputEvent::NumLockOn;
89   return modifiers;
90 }
91
92 ui::KeyboardCode GdkEventToWindowsKeyCode(const GdkEventKey* event) {
93   static const unsigned int kHardwareCodeToGDKKeyval[] = {
94     0,                 // 0x00:
95     0,                 // 0x01:
96     0,                 // 0x02:
97     0,                 // 0x03:
98     0,                 // 0x04:
99     0,                 // 0x05:
100     0,                 // 0x06:
101     0,                 // 0x07:
102     0,                 // 0x08:
103     0,                 // 0x09: GDK_Escape
104     GDK_1,             // 0x0A: GDK_1
105     GDK_2,             // 0x0B: GDK_2
106     GDK_3,             // 0x0C: GDK_3
107     GDK_4,             // 0x0D: GDK_4
108     GDK_5,             // 0x0E: GDK_5
109     GDK_6,             // 0x0F: GDK_6
110     GDK_7,             // 0x10: GDK_7
111     GDK_8,             // 0x11: GDK_8
112     GDK_9,             // 0x12: GDK_9
113     GDK_0,             // 0x13: GDK_0
114     GDK_minus,         // 0x14: GDK_minus
115     GDK_equal,         // 0x15: GDK_equal
116     0,                 // 0x16: GDK_BackSpace
117     0,                 // 0x17: GDK_Tab
118     GDK_q,             // 0x18: GDK_q
119     GDK_w,             // 0x19: GDK_w
120     GDK_e,             // 0x1A: GDK_e
121     GDK_r,             // 0x1B: GDK_r
122     GDK_t,             // 0x1C: GDK_t
123     GDK_y,             // 0x1D: GDK_y
124     GDK_u,             // 0x1E: GDK_u
125     GDK_i,             // 0x1F: GDK_i
126     GDK_o,             // 0x20: GDK_o
127     GDK_p,             // 0x21: GDK_p
128     GDK_bracketleft,   // 0x22: GDK_bracketleft
129     GDK_bracketright,  // 0x23: GDK_bracketright
130     0,                 // 0x24: GDK_Return
131     0,                 // 0x25: GDK_Control_L
132     GDK_a,             // 0x26: GDK_a
133     GDK_s,             // 0x27: GDK_s
134     GDK_d,             // 0x28: GDK_d
135     GDK_f,             // 0x29: GDK_f
136     GDK_g,             // 0x2A: GDK_g
137     GDK_h,             // 0x2B: GDK_h
138     GDK_j,             // 0x2C: GDK_j
139     GDK_k,             // 0x2D: GDK_k
140     GDK_l,             // 0x2E: GDK_l
141     GDK_semicolon,     // 0x2F: GDK_semicolon
142     GDK_apostrophe,    // 0x30: GDK_apostrophe
143     GDK_grave,         // 0x31: GDK_grave
144     0,                 // 0x32: GDK_Shift_L
145     GDK_backslash,     // 0x33: GDK_backslash
146     GDK_z,             // 0x34: GDK_z
147     GDK_x,             // 0x35: GDK_x
148     GDK_c,             // 0x36: GDK_c
149     GDK_v,             // 0x37: GDK_v
150     GDK_b,             // 0x38: GDK_b
151     GDK_n,             // 0x39: GDK_n
152     GDK_m,             // 0x3A: GDK_m
153     GDK_comma,         // 0x3B: GDK_comma
154     GDK_period,        // 0x3C: GDK_period
155     GDK_slash,         // 0x3D: GDK_slash
156     0,                 // 0x3E: GDK_Shift_R
157     0,                 // 0x3F:
158     0,                 // 0x40:
159     0,                 // 0x41:
160     0,                 // 0x42:
161     0,                 // 0x43:
162     0,                 // 0x44:
163     0,                 // 0x45:
164     0,                 // 0x46:
165     0,                 // 0x47:
166     0,                 // 0x48:
167     0,                 // 0x49:
168     0,                 // 0x4A:
169     0,                 // 0x4B:
170     0,                 // 0x4C:
171     0,                 // 0x4D:
172     0,                 // 0x4E:
173     0,                 // 0x4F:
174     0,                 // 0x50:
175     0,                 // 0x51:
176     0,                 // 0x52:
177     0,                 // 0x53:
178     0,                 // 0x54:
179     0,                 // 0x55:
180     0,                 // 0x56:
181     0,                 // 0x57:
182     0,                 // 0x58:
183     0,                 // 0x59:
184     0,                 // 0x5A:
185     0,                 // 0x5B:
186     0,                 // 0x5C:
187     0,                 // 0x5D:
188     0,                 // 0x5E:
189     0,                 // 0x5F:
190     0,                 // 0x60:
191     0,                 // 0x61:
192     0,                 // 0x62:
193     0,                 // 0x63:
194     0,                 // 0x64:
195     0,                 // 0x65:
196     0,                 // 0x66:
197     0,                 // 0x67:
198     0,                 // 0x68:
199     0,                 // 0x69:
200     0,                 // 0x6A:
201     0,                 // 0x6B:
202     0,                 // 0x6C:
203     0,                 // 0x6D:
204     0,                 // 0x6E:
205     0,                 // 0x6F:
206     0,                 // 0x70:
207     0,                 // 0x71:
208     0,                 // 0x72:
209     GDK_Super_L,       // 0x73: GDK_Super_L
210     GDK_Super_R,       // 0x74: GDK_Super_R
211   };
212
213   // |windows_key_code| has to include a valid virtual-key code even when we
214   // use non-US layouts, e.g. even when we type an 'A' key of a US keyboard
215   // on the Hebrew layout, |windows_key_code| should be VK_A.
216   // On the other hand, |event->keyval| value depends on the current
217   // GdkKeymap object, i.e. when we type an 'A' key of a US keyboard on
218   // the Hebrew layout, |event->keyval| becomes GDK_hebrew_shin and this
219   // ui::WindowsKeyCodeForGdkKeyCode() call returns 0.
220   // To improve compatibilty with Windows, we use |event->hardware_keycode|
221   // for retrieving its Windows key-code for the keys when the
222   // WebCore::windows_key_codeForEvent() call returns 0.
223   // We shouldn't use |event->hardware_keycode| for keys that GdkKeymap
224   // objects cannot change because |event->hardware_keycode| doesn't change
225   // even when we change the layout options, e.g. when we swap a control
226   // key and a caps-lock key, GTK doesn't swap their
227   // |event->hardware_keycode| values but swap their |event->keyval| values.
228   ui::KeyboardCode windows_key_code =
229       ui::WindowsKeyCodeForGdkKeyCode(event->keyval);
230   if (windows_key_code)
231     return windows_key_code;
232
233   if (event->hardware_keycode < arraysize(kHardwareCodeToGDKKeyval)) {
234     int keyval = kHardwareCodeToGDKKeyval[event->hardware_keycode];
235     if (keyval)
236       return ui::WindowsKeyCodeForGdkKeyCode(keyval);
237   }
238
239   // This key is one that keyboard-layout drivers cannot change.
240   // Use |event->keyval| to retrieve its |windows_key_code| value.
241   return ui::WindowsKeyCodeForGdkKeyCode(event->keyval);
242 }
243
244 // Normalizes event->state to make it Windows/Mac compatible. Since the way
245 // of setting modifier mask on X is very different than Windows/Mac as shown
246 // in http://crbug.com/127142#c8, the normalization is necessary.
247 guint NormalizeEventState(const GdkEventKey* event) {
248   guint mask = 0;
249   switch (GdkEventToWindowsKeyCode(event)) {
250     case ui::VKEY_CONTROL:
251     case ui::VKEY_LCONTROL:
252     case ui::VKEY_RCONTROL:
253       mask = GDK_CONTROL_MASK;
254       break;
255     case ui::VKEY_SHIFT:
256     case ui::VKEY_LSHIFT:
257     case ui::VKEY_RSHIFT:
258       mask = GDK_SHIFT_MASK;
259       break;
260     case ui::VKEY_MENU:
261     case ui::VKEY_LMENU:
262     case ui::VKEY_RMENU:
263       mask = GDK_MOD1_MASK;
264       break;
265     case ui::VKEY_CAPITAL:
266       mask = GDK_LOCK_MASK;
267       break;
268     default:
269       return event->state;
270   }
271   if (event->type == GDK_KEY_PRESS)
272     return event->state | mask;
273   return event->state & ~mask;
274 }
275
276 // Gets the corresponding control character of a specified key code. See:
277 // http://en.wikipedia.org/wiki/Control_characters
278 // We emulate Windows behavior here.
279 int GetControlCharacter(ui::KeyboardCode windows_key_code, bool shift) {
280   if (windows_key_code >= ui::VKEY_A && windows_key_code <= ui::VKEY_Z) {
281     // ctrl-A ~ ctrl-Z map to \x01 ~ \x1A
282     return windows_key_code - ui::VKEY_A + 1;
283   }
284   if (shift) {
285     // following graphics chars require shift key to input.
286     switch (windows_key_code) {
287       // ctrl-@ maps to \x00 (Null byte)
288       case ui::VKEY_2:
289         return 0;
290       // ctrl-^ maps to \x1E (Record separator, Information separator two)
291       case ui::VKEY_6:
292         return 0x1E;
293       // ctrl-_ maps to \x1F (Unit separator, Information separator one)
294       case ui::VKEY_OEM_MINUS:
295         return 0x1F;
296       // Returns 0 for all other keys to avoid inputting unexpected chars.
297       default:
298         return 0;
299     }
300   } else {
301     switch (windows_key_code) {
302       // ctrl-[ maps to \x1B (Escape)
303       case ui::VKEY_OEM_4:
304         return 0x1B;
305       // ctrl-\ maps to \x1C (File separator, Information separator four)
306       case ui::VKEY_OEM_5:
307         return 0x1C;
308       // ctrl-] maps to \x1D (Group separator, Information separator three)
309       case ui::VKEY_OEM_6:
310         return 0x1D;
311       // ctrl-Enter maps to \x0A (Line feed)
312       case ui::VKEY_RETURN:
313         return 0x0A;
314       // Returns 0 for all other keys to avoid inputting unexpected chars.
315       default:
316         return 0;
317     }
318   }
319 }
320
321 }  // namespace
322
323 namespace content {
324
325 // WebKeyboardEvent -----------------------------------------------------------
326
327 WebKeyboardEvent WebKeyboardEventBuilder::Build(const GdkEventKey* event) {
328   WebKeyboardEvent result;
329
330   result.timeStampSeconds = GdkEventTimeToWebEventTime(event->time);
331   result.modifiers = GdkStateToWebEventModifiers(NormalizeEventState(event));
332
333   switch (event->type) {
334     case GDK_KEY_RELEASE:
335       result.type = WebInputEvent::KeyUp;
336       break;
337     case GDK_KEY_PRESS:
338       result.type = WebInputEvent::RawKeyDown;
339       break;
340     default:
341       NOTREACHED();
342   }
343
344   // According to MSDN:
345   // http://msdn.microsoft.com/en-us/library/ms646286(VS.85).aspx
346   // Key events with Alt modifier and F10 are system key events.
347   // We just emulate this behavior. It's necessary to prevent webkit from
348   // processing keypress event generated by alt-d, etc.
349   // F10 is not special on Linux, so don't treat it as system key.
350   if (result.modifiers & WebInputEvent::AltKey)
351     result.isSystemKey = true;
352
353   // The key code tells us which physical key was pressed (for example, the
354   // A key went down or up).  It does not determine whether A should be lower
355   // or upper case.  This is what text does, which should be the keyval.
356   ui::KeyboardCode windows_key_code = GdkEventToWindowsKeyCode(event);
357   result.windowsKeyCode = GetWindowsKeyCodeWithoutLocation(windows_key_code);
358   result.modifiers |= GetLocationModifiersFromWindowsKeyCode(windows_key_code);
359   result.nativeKeyCode = event->hardware_keycode;
360
361   if (result.windowsKeyCode == ui::VKEY_RETURN) {
362     // We need to treat the enter key as a key press of character \r.  This
363     // is apparently just how webkit handles it and what it expects.
364     result.unmodifiedText[0] = '\r';
365   } else {
366     // FIXME: fix for non BMP chars
367     result.unmodifiedText[0] =
368         static_cast<int>(gdk_keyval_to_unicode(event->keyval));
369   }
370
371   // If ctrl key is pressed down, then control character shall be input.
372   if (result.modifiers & WebInputEvent::ControlKey) {
373     result.text[0] =
374       GetControlCharacter(ui::KeyboardCode(result.windowsKeyCode),
375                           result.modifiers & WebInputEvent::ShiftKey);
376   } else {
377     result.text[0] = result.unmodifiedText[0];
378   }
379
380   result.setKeyIdentifierFromWindowsKeyCode();
381
382   // FIXME: Do we need to set IsAutoRepeat?
383   if (IsKeyPadKeyval(event->keyval))
384     result.modifiers |= WebInputEvent::IsKeyPad;
385
386   return result;
387 }
388
389 WebKeyboardEvent WebKeyboardEventBuilder::Build(wchar_t character,
390                                                      int state,
391                                                      double timeStampSeconds) {
392   // keyboardEvent(const GdkEventKey*) depends on the GdkEventKey object and
393   // it is hard to use/ it from signal handlers which don't use GdkEventKey
394   // objects (e.g. GtkIMContext signal handlers.) For such handlers, this
395   // function creates a WebInputEvent::Char event without using a
396   // GdkEventKey object.
397   WebKeyboardEvent result;
398   result.type = WebKit::WebInputEvent::Char;
399   result.timeStampSeconds = timeStampSeconds;
400   result.modifiers = GdkStateToWebEventModifiers(state);
401   result.windowsKeyCode = character;
402   result.nativeKeyCode = character;
403   result.text[0] = character;
404   result.unmodifiedText[0] = character;
405
406   // According to MSDN:
407   // http://msdn.microsoft.com/en-us/library/ms646286(VS.85).aspx
408   // Key events with Alt modifier and F10 are system key events.
409   // We just emulate this behavior. It's necessary to prevent webkit from
410   // processing keypress event generated by alt-d, etc.
411   // F10 is not special on Linux, so don't treat it as system key.
412   if (result.modifiers & WebInputEvent::AltKey)
413     result.isSystemKey = true;
414
415   return result;
416 }
417
418 // WebMouseEvent --------------------------------------------------------------
419
420 WebMouseEvent WebMouseEventBuilder::Build(const GdkEventButton* event) {
421   WebMouseEvent result;
422
423   result.timeStampSeconds = GdkEventTimeToWebEventTime(event->time);
424
425   result.modifiers = GdkStateToWebEventModifiers(event->state);
426   result.x = static_cast<int>(event->x);
427   result.y = static_cast<int>(event->y);
428   result.windowX = result.x;
429   result.windowY = result.y;
430   result.globalX = static_cast<int>(event->x_root);
431   result.globalY = static_cast<int>(event->y_root);
432   result.clickCount = 0;
433
434   switch (event->type) {
435     case GDK_BUTTON_PRESS:
436       result.type = WebInputEvent::MouseDown;
437       break;
438     case GDK_BUTTON_RELEASE:
439       result.type = WebInputEvent::MouseUp;
440       break;
441     case GDK_3BUTTON_PRESS:
442     case GDK_2BUTTON_PRESS:
443     default:
444       NOTREACHED();
445   }
446
447   result.button = WebMouseEvent::ButtonNone;
448   if (event->button == 1)
449     result.button = WebMouseEvent::ButtonLeft;
450   else if (event->button == 2)
451     result.button = WebMouseEvent::ButtonMiddle;
452   else if (event->button == 3)
453     result.button = WebMouseEvent::ButtonRight;
454
455   if (result.type == WebInputEvent::MouseDown) {
456     bool forgetPreviousClick = ShouldForgetPreviousClick(
457         event->window, event->time, event->x, event->y);
458
459     if (!forgetPreviousClick && result.button == last_click_button) {
460       ++num_clicks;
461     } else {
462       num_clicks = 1;
463
464       last_click_event_window = event->window;
465       last_click_x = event->x;
466       last_click_y = event->y;
467       last_click_button = result.button;
468     }
469     last_click_time = event->time;
470   }
471   result.clickCount = num_clicks;
472
473   return result;
474 }
475
476 WebMouseEvent WebMouseEventBuilder::Build(const GdkEventMotion* event) {
477   WebMouseEvent result;
478
479   result.timeStampSeconds = GdkEventTimeToWebEventTime(event->time);
480   result.modifiers = GdkStateToWebEventModifiers(event->state);
481   result.x = static_cast<int>(event->x);
482   result.y = static_cast<int>(event->y);
483   result.windowX = result.x;
484   result.windowY = result.y;
485   result.globalX = static_cast<int>(event->x_root);
486   result.globalY = static_cast<int>(event->y_root);
487
488   switch (event->type) {
489     case GDK_MOTION_NOTIFY:
490       result.type = WebInputEvent::MouseMove;
491       break;
492     default:
493       NOTREACHED();
494   }
495
496   result.button = WebMouseEvent::ButtonNone;
497   if (event->state & GDK_BUTTON1_MASK)
498     result.button = WebMouseEvent::ButtonLeft;
499   else if (event->state & GDK_BUTTON2_MASK)
500     result.button = WebMouseEvent::ButtonMiddle;
501   else if (event->state & GDK_BUTTON3_MASK)
502     result.button = WebMouseEvent::ButtonRight;
503
504   if (ShouldForgetPreviousClick(event->window, event->time, event->x, event->y))
505     ResetClickCountState();
506
507   return result;
508 }
509
510 WebMouseEvent WebMouseEventBuilder::Build(const GdkEventCrossing* event) {
511   WebMouseEvent result;
512
513   result.timeStampSeconds = GdkEventTimeToWebEventTime(event->time);
514   result.modifiers = GdkStateToWebEventModifiers(event->state);
515   result.x = static_cast<int>(event->x);
516   result.y = static_cast<int>(event->y);
517   result.windowX = result.x;
518   result.windowY = result.y;
519   result.globalX = static_cast<int>(event->x_root);
520   result.globalY = static_cast<int>(event->y_root);
521
522   switch (event->type) {
523     case GDK_ENTER_NOTIFY:
524     case GDK_LEAVE_NOTIFY:
525       // Note that if we sent MouseEnter or MouseLeave to WebKit, it
526       // wouldn't work - they don't result in the proper JavaScript events.
527       // MouseMove does the right thing.
528       result.type = WebInputEvent::MouseMove;
529       break;
530     default:
531       NOTREACHED();
532   }
533
534   result.button = WebMouseEvent::ButtonNone;
535   if (event->state & GDK_BUTTON1_MASK)
536     result.button = WebMouseEvent::ButtonLeft;
537   else if (event->state & GDK_BUTTON2_MASK)
538     result.button = WebMouseEvent::ButtonMiddle;
539   else if (event->state & GDK_BUTTON3_MASK)
540     result.button = WebMouseEvent::ButtonRight;
541
542   if (ShouldForgetPreviousClick(event->window, event->time, event->x, event->y))
543     ResetClickCountState();
544
545   return result;
546 }
547
548 // WebMouseWheelEvent ---------------------------------------------------------
549
550 float WebMouseWheelEventBuilder::ScrollbarPixelsPerTick() {
551   // How much should we scroll per mouse wheel event?
552   // - Windows uses 3 lines by default and obeys a system setting.
553   // - Mozilla has a pref that lets you either use the "system" number of lines
554   //   to scroll, or lets the user override it.
555   //   For the "system" number of lines, it appears they've hardcoded 3.
556   //   See case NS_MOUSE_SCROLL in content/events/src/nsEventStateManager.cpp
557   //   and InitMouseScrollEvent in widget/src/gtk2/nsCommonWidget.cpp .
558   // - Gtk makes the scroll amount a function of the size of the scroll bar,
559   //   which is not available to us here.
560   // Instead, we pick a number that empirically matches Firefox's behavior.
561   return 160.0f / 3.0f;
562 }
563
564 WebMouseWheelEvent WebMouseWheelEventBuilder::Build(
565     const GdkEventScroll* event) {
566   WebMouseWheelEvent result;
567
568   result.type = WebInputEvent::MouseWheel;
569   result.button = WebMouseEvent::ButtonNone;
570
571   result.timeStampSeconds = GdkEventTimeToWebEventTime(event->time);
572   result.modifiers = GdkStateToWebEventModifiers(event->state);
573   result.x = static_cast<int>(event->x);
574   result.y = static_cast<int>(event->y);
575   result.windowX = result.x;
576   result.windowY = result.y;
577   result.globalX = static_cast<int>(event->x_root);
578   result.globalY = static_cast<int>(event->y_root);
579
580   static const float scrollbarPixelsPerTick = ScrollbarPixelsPerTick();
581   switch (event->direction) {
582     case GDK_SCROLL_UP:
583       result.deltaY = scrollbarPixelsPerTick;
584       result.wheelTicksY = 1;
585       break;
586     case GDK_SCROLL_DOWN:
587       result.deltaY = -scrollbarPixelsPerTick;
588       result.wheelTicksY = -1;
589       break;
590     case GDK_SCROLL_LEFT:
591       result.deltaX = scrollbarPixelsPerTick;
592       result.wheelTicksX = 1;
593       break;
594     case GDK_SCROLL_RIGHT:
595       result.deltaX = -scrollbarPixelsPerTick;
596       result.wheelTicksX = -1;
597       break;
598   }
599
600   return result;
601 }
602
603 }  // namespace content