Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(289)

Side by Side Diff: third_party/WebKit/Source/core/input/TouchEventManager.cpp

Issue 1892653003: Extract touch handling logic from EventHandler (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Add TODO Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 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 "core/input/TouchEventManager.h"
6
7 #include "core/dom/Document.h"
8 #include "core/events/TouchEvent.h"
9 #include "core/frame/EventHandlerRegistry.h"
10 #include "core/frame/FrameHost.h"
11 #include "core/frame/FrameView.h"
12 #include "core/html/HTMLCanvasElement.h"
13 #include "core/input/EventHandler.h"
14 #include "core/input/TouchActionUtil.h"
15 #include "core/page/ChromeClient.h"
16 #include "core/page/Page.h"
17 #include "platform/Histogram.h"
18 #include "platform/PlatformTouchEvent.h"
19
20
21
22 namespace blink {
23
24 namespace {
25
26 bool hasTouchHandlers(const EventHandlerRegistry& registry)
27 {
28 return registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEvent Blocking)
29 || registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEvent Passive)
30 || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEvent Blocking)
31 || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEvent Passive);
32 }
33
34 const AtomicString& touchEventNameForTouchPointState(PlatformTouchPoint::TouchSt ate state)
35 {
36 switch (state) {
37 case PlatformTouchPoint::TouchReleased:
38 return EventTypeNames::touchend;
39 case PlatformTouchPoint::TouchCancelled:
40 return EventTypeNames::touchcancel;
41 case PlatformTouchPoint::TouchPressed:
42 return EventTypeNames::touchstart;
43 case PlatformTouchPoint::TouchMoved:
44 return EventTypeNames::touchmove;
45 case PlatformTouchPoint::TouchStationary:
46 // Fall through to default
47 default:
48 ASSERT_NOT_REACHED();
49 return emptyAtom;
50 }
51 }
52
53 // These offsets change indicies into the ListenerHistogram
54 // enumeration. The addition of a series of offsets then
55 // produces the resulting ListenerHistogram value.
56 const size_t kTouchTargetHistogramRootScrollerOffset = 4;
57 const size_t kTouchTargetHistogramScrollableDocumentOffset = 2;
58 const size_t kTouchTargetHistogramHandledOffset = 1;
59
60 enum TouchTargetAndDispatchResultType {
61 NonRootScrollerNonScrollableNotHandled, // Non-root-scroller, non-scrollable document, not handled.
62 NonRootScrollerNonScrollableHandled, // Non-root-scroller, non-scrollable do cument, handled application.
63 NonRootScrollerScrollableDocumentNotHandled, // Non-root-scroller, scrollabl e document, not handled.
64 NonRootScrollerScrollableDocumentHandled, // Non-root-scroller, scrollable d ocument, handled application.
65 RootScrollerNonScrollableNotHandled, // Root-scroller, non-scrollable docume nt, not handled.
66 RootScrollerNonScrollableHandled, // Root-scroller, non-scrollable document, handled.
67 RootScrollerScrollableDocumentNotHandled, // Root-scroller, scrollable docum ent, not handled.
68 RootScrollerScrollableDocumentHandled, // Root-scroller, scrollable document , handled.
69 TouchTargetAndDispatchResultTypeMax,
70 };
71
72 TouchTargetAndDispatchResultType toTouchTargetHistogramValue(EventTarget* eventT arget, DispatchEventResult dispatchResult)
73 {
74 int result = 0;
75 Document* document = nullptr;
76
77 if (const LocalDOMWindow* domWindow = eventTarget->toLocalDOMWindow()) {
78 // Treat the window as a root scroller as well.
79 document = domWindow->document();
80 result += kTouchTargetHistogramRootScrollerOffset;
81 } else if (Node* node = eventTarget->toNode()) {
82 // Report if the target node is the document or body.
83 if (node->isDocumentNode() || static_cast<Node*>(node->document().docume ntElement()) == node || static_cast<Node*>(node->document().body()) == node) {
84 result += kTouchTargetHistogramRootScrollerOffset;
85 }
86 document = &node->document();
87 }
88
89 if (document) {
90 FrameView* view = document->view();
91 if (view && view->isScrollable())
92 result += kTouchTargetHistogramScrollableDocumentOffset;
93 }
94
95 if (dispatchResult != DispatchEventResult::NotCanceled)
96 result += kTouchTargetHistogramHandledOffset;
97 return static_cast<TouchTargetAndDispatchResultType>(result);
98 }
99
100 enum TouchEventDispatchResultType {
101 UnhandledTouches, // Unhandled touch events.
102 HandledTouches, // Handled touch events.
103 TouchEventDispatchResultTypeMax,
104 };
105
106 // Defining this class type local to dispatchTouchEvents() and annotating
107 // it with STACK_ALLOCATED(), runs into MSVC(VS 2013)'s C4822 warning
108 // that the local class doesn't provide a local definition for 'operator new'.
109 // Which it intentionally doesn't and shouldn't.
110 //
111 // Work around such toolchain bugginess by lifting out the type, thereby
112 // taking it out of C4822's reach.
113 class ChangedTouches final {
114 STACK_ALLOCATED();
115 public:
116 // The touches corresponding to the particular change state this struct
117 // instance represents.
118 Member<TouchList> m_touches;
119
120 using EventTargetSet = HeapHashSet<Member<EventTarget>>;
121 // Set of targets involved in m_touches.
122 EventTargetSet m_targets;
123 };
124
125 } // namespace
126
127 TouchEventManager::TouchEventManager(LocalFrame* frame)
128 : m_frame(frame)
129 {
130 clear();
131 }
132
133 TouchEventManager::~TouchEventManager()
134 {
135 }
136
137 WebInputEventResult TouchEventManager::dispatchTouchEvents(
138 const PlatformTouchEvent& event,
139 const HeapVector<TouchInfo>& touchInfos,
140 bool allTouchesReleased)
141 {
142 bool touchStartOrFirstTouchMove = false;
143 if (event.type() == PlatformEvent::TouchStart) {
144 m_waitingForFirstTouchMove = true;
145 touchStartOrFirstTouchMove = true;
146 } else if (event.type() == PlatformEvent::TouchMove) {
147 touchStartOrFirstTouchMove = m_waitingForFirstTouchMove;
148 m_waitingForFirstTouchMove = false;
149 }
150
151 // Build up the lists to use for the |touches|, |targetTouches| and
152 // |changedTouches| attributes in the JS event. See
153 // http://www.w3.org/TR/touch-events/#touchevent-interface for how these
154 // lists fit together.
155
156 // Holds the complete set of touches on the screen.
157 TouchList* touches = TouchList::create();
158
159 // A different view on the 'touches' list above, filtered and grouped by
160 // event target. Used for the |targetTouches| list in the JS event.
161 using TargetTouchesHeapMap = HeapHashMap<EventTarget*, Member<TouchList>>;
162 TargetTouchesHeapMap touchesByTarget;
163
164 // Array of touches per state, used to assemble the |changedTouches| list.
165 ChangedTouches changedTouches[PlatformTouchPoint::TouchStateEnd];
166
167 for (unsigned i = 0; i < touchInfos.size(); ++i) {
168 const TouchInfo& touchInfo = touchInfos[i];
169 const PlatformTouchPoint& point = touchInfo.point;
170 PlatformTouchPoint::TouchState pointState = point.state();
171
172 if (touchInfo.consumed)
173 continue;
174
175 Touch* touch = Touch::create(
176 touchInfo.targetFrame.get(),
177 touchInfo.touchNode.get(),
178 point.id(),
179 point.screenPos(),
180 touchInfo.adjustedPagePoint,
181 touchInfo.adjustedRadius,
182 point.rotationAngle(),
183 point.force(),
184 touchInfo.region);
185
186 // Ensure this target's touch list exists, even if it ends up empty, so
187 // it can always be passed to TouchEvent::Create below.
188 TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.f ind(touchInfo.touchNode.get());
189 if (targetTouchesIterator == touchesByTarget.end()) {
190 touchesByTarget.set(touchInfo.touchNode.get(), TouchList::create());
191 targetTouchesIterator = touchesByTarget.find(touchInfo.touchNode.get ());
192 }
193
194 // |touches| and |targetTouches| should only contain information about
195 // touches still on the screen, so if this point is released or
196 // cancelled it will only appear in the |changedTouches| list.
197 if (pointState != PlatformTouchPoint::TouchReleased && pointState != Pla tformTouchPoint::TouchCancelled) {
198 touches->append(touch);
199 targetTouchesIterator->value->append(touch);
200 }
201
202 // Now build up the correct list for |changedTouches|.
203 // Note that any touches that are in the TouchStationary state (e.g. if
204 // the user had several points touched but did not move them all) should
205 // never be in the |changedTouches| list so we do not handle them
206 // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
207 // for further discussion about the TouchStationary state.
208 if (pointState != PlatformTouchPoint::TouchStationary && touchInfo.known Target) {
209 ASSERT(pointState < PlatformTouchPoint::TouchStateEnd);
210 if (!changedTouches[pointState].m_touches)
211 changedTouches[pointState].m_touches = TouchList::create();
212 changedTouches[pointState].m_touches->append(touch);
213 changedTouches[pointState].m_targets.add(touchInfo.touchNode);
214 }
215 }
216
217 if (allTouchesReleased) {
218 m_touchSequenceDocument.clear();
219 m_touchSequenceUserGestureToken.clear();
220 }
221
222 WebInputEventResult eventResult = WebInputEventResult::NotHandled;
223
224 // Now iterate through the |changedTouches| list and |m_targets| within it,
225 // sending TouchEvents to the targets as required.
226 for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state ) {
227 if (!changedTouches[state].m_touches)
228 continue;
229
230 const AtomicString& eventName(touchEventNameForTouchPointState(static_ca st<PlatformTouchPoint::TouchState>(state)));
231 for (const auto& eventTarget : changedTouches[state].m_targets) {
232 EventTarget* touchEventTarget = eventTarget;
233 TouchEvent* touchEvent = TouchEvent::create(
234 touches, touchesByTarget.get(touchEventTarget), changedTouches[s tate].m_touches.get(),
235 eventName, touchEventTarget->toNode()->document().domWindow(),
236 event.getModifiers(), event.cancelable(), event.causesScrollingI fUncanceled(), event.timestamp());
237
238 DispatchEventResult domDispatchResult = touchEventTarget->dispatchEv ent(touchEvent);
239
240 // Only report for top level documents with a single touch on
241 // touch-start or the first touch-move.
242 if (touchStartOrFirstTouchMove && touchInfos.size() == 1 && event.ca ncelable() && !m_frame->document()->ownerElement()) {
243 DEFINE_STATIC_LOCAL(EnumerationHistogram, rootDocumentListenerHi stogram, ("Event.Touch.TargetAndDispatchResult", TouchTargetAndDispatchResultTyp eMax));
244 rootDocumentListenerHistogram.count(toTouchTargetHistogramValue( eventTarget, domDispatchResult));
245
246 // Count the handled touch starts and first touch moves before a nd after the page is fully loaded respectively.
247 if (m_frame->document()->isLoadCompleted()) {
248 DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsA fterPageLoadHistogram, ("Event.Touch.TouchDispositionsAfterPageLoad", TouchEvent DispatchResultTypeMax));
249 touchDispositionsAfterPageLoadHistogram.count((domDispatchRe sult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches);
250 } else {
251 DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsB eforePageLoadHistogram, ("Event.Touch.TouchDispositionsBeforePageLoad", TouchEve ntDispatchResultTypeMax));
252 touchDispositionsBeforePageLoadHistogram.count((domDispatchR esult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches);
253 }
254 }
255 eventResult = EventHandler::mergeEventResult(eventResult,
256 EventHandler::toWebInputEventResult(domDispatchResult));
257 }
258 }
259 return eventResult;
260 }
261
262 void TouchEventManager::updateTargetAndRegionMapsForTouchStarts(
263 HeapVector<TouchInfo>& touchInfos)
264 {
265 for (auto& touchInfo : touchInfos) {
266 // Touch events implicitly capture to the touched node, and don't change
267 // active/hover states themselves (Gesture events do). So we only need
268 // to hit-test on touchstart and when the target could be different than
269 // the corresponding pointer event target.
270 if (touchInfo.point.state() == PlatformTouchPoint::TouchPressed) {
271 HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEv ent | HitTestRequest::ReadOnly | HitTestRequest::Active;
272 LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->rootFram eToContents(touchInfo.point.pos()));
273 HitTestResult result;
274 if (!m_touchSequenceDocument) {
275 result = m_frame->eventHandler().hitTestResultAtPoint(pagePoint, hitType);
276 } else if (m_touchSequenceDocument->frame()) {
277 LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocum ent->frame()->view()->rootFrameToContents(touchInfo.point.pos()));
278 result = EventHandler::hitTestResultInFrame(m_touchSequenceDocum ent->frame(), framePoint, hitType);
279 } else {
280 continue;
281 }
282
283 Node* node = result.innerNode();
284 if (!node)
285 continue;
286 if (isHTMLCanvasElement(node)) {
287 std::pair<Element*, String> regionInfo = toHTMLCanvasElement(nod e)->getControlAndIdIfHitRegionExists(result.pointInInnerNodeFrame());
288 if (regionInfo.first)
289 node = regionInfo.first;
290 touchInfo.region = regionInfo.second;
291 }
292 // Touch events should not go to text nodes.
293 if (node->isTextNode())
294 node = FlatTreeTraversal::parent(*node);
295 touchInfo.touchNode = node;
296
297 if (!m_touchSequenceDocument) {
298 // Keep track of which document should receive all touch events
299 // in the active sequence. This must be a single document to
300 // ensure we don't leak Nodes between documents.
301 m_touchSequenceDocument = &(touchInfo.touchNode->document());
302 ASSERT(m_touchSequenceDocument->frame()->view());
303 }
304
305 // Ideally we'd ASSERT(!m_targetForTouchID.contains(point.id())
306 // since we shouldn't get a touchstart for a touch that's already
307 // down. However EventSender allows this to be violated and there's
308 // some tests that take advantage of it. There may also be edge
309 // cases in the browser where this happens.
310 // See http://crbug.com/345372.
311 m_targetForTouchID.set(touchInfo.point.id(), touchInfo.touchNode);
312
313 m_regionForTouchID.set(touchInfo.point.id(), touchInfo.region);
314
315 TouchAction effectiveTouchAction =
316 TouchActionUtil::computeEffectiveTouchAction(
317 *touchInfo.touchNode);
318 if (effectiveTouchAction != TouchActionAuto)
319 m_frame->page()->chromeClient().setTouchAction(effectiveTouchAct ion);
320 }
321 }
322 }
323
324 void TouchEventManager::setAllPropertiesOfTouchInfos(
325 HeapVector<TouchInfo>& touchInfos)
326 {
327 for (auto& touchInfo : touchInfos) {
328 PlatformTouchPoint::TouchState pointState = touchInfo.point.state();
329 Node* touchNode = nullptr;
330 String regionID;
331
332 if (pointState == PlatformTouchPoint::TouchReleased
333 || pointState == PlatformTouchPoint::TouchCancelled) {
334 // The target should be the original target for this touch, so get
335 // it from the hashmap. As it's a release or cancel we also remove
336 // it from the map.
337 touchNode = m_targetForTouchID.take(touchInfo.point.id());
338 regionID = m_regionForTouchID.take(touchInfo.point.id());
339 } else {
340 // No hittest is performed on move or stationary, since the target
341 // is not allowed to change anyway.
342 touchNode = m_targetForTouchID.get(touchInfo.point.id());
343 regionID = m_regionForTouchID.get(touchInfo.point.id());
344 }
345
346 LocalFrame* targetFrame = nullptr;
347 bool knownTarget = false;
348 if (touchNode) {
349 Document& doc = touchNode->document();
350 // If the target node has moved to a new document while it was being touched,
351 // we can't send events to the new document because that could leak nodes
352 // from one document to another. See http://crbug.com/394339.
353 if (&doc == m_touchSequenceDocument.get()) {
354 targetFrame = doc.frame();
355 knownTarget = true;
356 }
357 }
358 if (!knownTarget) {
359 // If we don't have a target registered for the point it means we've
360 // missed our opportunity to do a hit test for it (due to some
361 // optimization that prevented blink from ever seeing the
362 // touchstart), or that the touch started outside the active touch
363 // sequence document. We should still include the touch in the
364 // Touches list reported to the application (eg. so it can
365 // differentiate between a one and two finger gesture), but we won't
366 // actually dispatch any events for it. Set the target to the
367 // Document so that there's some valid node here. Perhaps this
368 // should really be LocalDOMWindow, but in all other cases the targe t of
369 // a Touch is a Node so using the window could be a breaking change.
370 // Since we know there was no handler invoked, the specific target
371 // should be completely irrelevant to the application.
372 touchNode = m_touchSequenceDocument;
373 targetFrame = m_touchSequenceDocument->frame();
374 }
375 ASSERT(targetFrame);
376
377 // pagePoint should always be in the target element's document coordinat es.
378 FloatPoint pagePoint = targetFrame->view()->rootFrameToContents(
379 touchInfo.point.pos());
380 float scaleFactor = 1.0f / targetFrame->pageZoomFactor();
381
382 touchInfo.touchNode = touchNode;
383 touchInfo.targetFrame = targetFrame;
384 touchInfo.adjustedPagePoint = pagePoint.scaledBy(scaleFactor);
385 touchInfo.adjustedRadius = touchInfo.point.radius().scaledBy(scaleFactor );
386 touchInfo.knownTarget = knownTarget;
387 touchInfo.consumed = false;
388 touchInfo.region = regionID;
389 }
390 }
391
392 bool TouchEventManager::generateTouchInfosAfterHittest(
393 const PlatformTouchEvent& event,
394 HeapVector<TouchInfo>& touchInfos)
395 {
396 bool newTouchSequence = true;
397 bool allTouchesReleased = true;
398
399 for (const auto& point : event.touchPoints()) {
400 if (point.state() != PlatformTouchPoint::TouchPressed)
401 newTouchSequence = false;
402 if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
403 allTouchesReleased = false;
404 }
405 if (newTouchSequence) {
406 // Ideally we'd ASSERT(!m_touchSequenceDocument) here since we should
407 // have cleared the active document when we saw the last release. But we
408 // have some tests that violate this, ClusterFuzz could trigger it, and
409 // there may be cases where the browser doesn't reliably release all
410 // touches. http://crbug.com/345372 tracks this.
411 m_touchSequenceDocument.clear();
412 m_touchSequenceUserGestureToken.clear();
413 }
414
415 ASSERT(m_frame->view());
416 if (m_touchSequenceDocument && (!m_touchSequenceDocument->frame() || !m_touc hSequenceDocument->frame()->view())) {
417 // If the active touch document has no frame or view, it's probably bein g destroyed
418 // so we can't dispatch events.
419 return false;
420 }
421
422 for (const auto& point : event.touchPoints()) {
423 TouchEventManager::TouchInfo touchInfo;
424 touchInfo.point = point;
425 touchInfos.append(touchInfo);
426 }
427
428 updateTargetAndRegionMapsForTouchStarts(touchInfos);
429
430 m_touchPressed = !allTouchesReleased;
431
432 // If there's no document receiving touch events, or no handlers on the
433 // document set to receive the events, then we can skip all the rest of
434 // this work.
435 if (!m_touchSequenceDocument || !m_touchSequenceDocument->frameHost() || !ha sTouchHandlers(m_touchSequenceDocument->frameHost()->eventHandlerRegistry()) || !m_touchSequenceDocument->frame()) {
436 if (allTouchesReleased) {
437 m_touchSequenceDocument.clear();
438 m_touchSequenceUserGestureToken.clear();
439 }
440 return false;
441 }
442
443 setAllPropertiesOfTouchInfos(touchInfos);
444
445 return true;
446 }
447
448 WebInputEventResult TouchEventManager::handleTouchEvent(
449 const PlatformTouchEvent& event,
450 const HeapVector<TouchInfo>& touchInfos)
451 {
452 // Note that the disposition of any pointer events affects only the generati on of touch
453 // events. If all pointer events were handled (and hence no touch events wer e fired), that
454 // is still equivalent to the touch events going unhandled because pointer e vent handler
455 // don't block scroll gesture generation.
456
457 // TODO(crbug.com/507408): If PE handlers always call preventDefault, we won 't see TEs until after
458 // scrolling starts because the scrolling would suppress upcoming PEs. This sudden "break" in TE
459 // suppression can make the visible TEs inconsistent (e.g. touchmove without a touchstart).
460
461 bool allTouchesReleased = true;
462 for (const auto& point : event.touchPoints()) {
463 if (point.state() != PlatformTouchPoint::TouchReleased
464 && point.state() != PlatformTouchPoint::TouchCancelled)
465 allTouchesReleased = false;
466 }
467
468 // Whether a touch should be considered a "user gesture" or not is a tricky question.
469 // https://docs.google.com/document/d/1oF1T3O7_E4t1PYHV6gyCwHxOi3ystm0eSL5xZ u7nvOg/edit#
470 // TODO(rbyers): Disable user gesture in some cases but retain logging for n ow (crbug.com/582140).
471 OwnPtr<UserGestureIndicator> gestureIndicator;
472 if (event.touchPoints().size() == 1
473 && event.touchPoints()[0].state() == PlatformTouchPoint::TouchReleased
474 && !event.causesScrollingIfUncanceled()) {
475 // This is a touchend corresponding to a tap, definitely a user gesture. So don't supply
476 // a UserGestureUtilizedCallback.
477 gestureIndicator = adoptPtr(new UserGestureIndicator(DefinitelyProcessin gUserGesture));
478 } else {
479 // This is some other touch event that perhaps shouldn't be considered a user gesture. So
480 // use a UserGestureUtilizedCallback to get metrics / deprecation warnin gs.
481 if (m_touchSequenceUserGestureToken)
482 gestureIndicator = adoptPtr(new UserGestureIndicator(m_touchSequence UserGestureToken.release(), &m_touchSequenceDocument->frame()->eventHandler()));
483 else
484 gestureIndicator = adoptPtr(new UserGestureIndicator(DefinitelyProce ssingUserGesture, &m_touchSequenceDocument->frame()->eventHandler()));
485 m_touchSequenceUserGestureToken = UserGestureIndicator::currentToken();
486 }
487
488 return dispatchTouchEvents(event, touchInfos, allTouchesReleased);
489 }
490
491 void TouchEventManager::clear()
492 {
493 m_touchSequenceDocument.clear();
494 m_touchSequenceUserGestureToken.clear();
495 m_targetForTouchID.clear();
496 m_regionForTouchID.clear();
497 m_touchPressed = false;
498 m_waitingForFirstTouchMove = false;
499 }
500
501 bool TouchEventManager::isAnyTouchActive() const
502 {
503 return m_touchPressed;
504 }
505
506 DEFINE_TRACE(TouchEventManager)
507 {
508 visitor->trace(m_frame);
509 visitor->trace(m_touchSequenceDocument);
510 visitor->trace(m_targetForTouchID);
511 }
512
513 } // namespace blink
OLDNEW
« no previous file with comments | « third_party/WebKit/Source/core/input/TouchEventManager.h ('k') | third_party/WebKit/Source/web/WebInputEventConversion.cpp » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698