1/*
2 * Copyright (C) 2010, 2011 Research In Motion Limited. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19#include "config.h"
20#include "TouchEventHandler.h"
21
22#include "DOMSupport.h"
23#include "Document.h"
24#include "DocumentMarkerController.h"
25#include "FatFingers.h"
26#include "FocusController.h"
27#include "Frame.h"
28#include "FrameView.h"
29#include "HTMLAnchorElement.h"
30#include "HTMLAreaElement.h"
31#include "HTMLImageElement.h"
32#include "HTMLInputElement.h"
33#include "HTMLNames.h"
34#include "HTMLPlugInElement.h"
35#include "InputHandler.h"
36#include "IntRect.h"
37#include "IntSize.h"
38#include "Node.h"
39#include "Page.h"
40#include "PlatformMouseEvent.h"
41#include "PlatformTouchEvent.h"
42#include "RenderLayer.h"
43#include "RenderTheme.h"
44#include "RenderView.h"
45#include "RenderedDocumentMarker.h"
46#include "SelectionHandler.h"
47#include "WebPage_p.h"
48#include "WebSettings.h"
49
50#include <wtf/MathExtras.h>
51
52using namespace WebCore;
53using namespace WTF;
54
55namespace BlackBerry {
56namespace WebKit {
57
58static bool hasMouseMoveListener(Element* element)
59{
60 ASSERT(element);
61 return element->hasEventListeners(eventNames().mousemoveEvent) || element->document()->hasEventListeners(eventNames().mousemoveEvent);
62}
63
64static bool hasTouchListener(Element* element)
65{
66 ASSERT(element);
67 return element->hasEventListeners(eventNames().touchstartEvent)
68 || element->hasEventListeners(eventNames().touchmoveEvent)
69 || element->hasEventListeners(eventNames().touchcancelEvent)
70 || element->hasEventListeners(eventNames().touchendEvent);
71}
72
73static bool elementExpectsMouseEvents(Element* element)
74{
75 // Make sure we are not operating a shadow node here, since the webpages
76 // aren't able to attach event listeners to shadow content.
77 while (element->isInShadowTree())
78 element = toElement(element->shadowAncestorNode());
79
80 return hasMouseMoveListener(element) && !hasTouchListener(element);
81}
82
83static bool shouldConvertTouchToMouse(Element* element)
84{
85 if (!element)
86 return false;
87
88 // Range element are a special case that require natural mouse events in order to allow
89 // dragging of the slider handle.
90 if (element->hasTagName(HTMLNames::inputTag)) {
91 HTMLInputElement* inputElement = static_cast<HTMLInputElement*>(element);
92 if (inputElement->isRangeControl())
93 return true;
94 }
95
96 // Check for plugin
97 if ((element->hasTagName(HTMLNames::objectTag) || element->hasTagName(HTMLNames::embedTag)) && static_cast<HTMLPlugInElement*>(element))
98 return true;
99
100 // Check if the element has a mouse listener and no touch listener. If so,
101 // the field will require touch events be converted to mouse events to function properly.
102 if (elementExpectsMouseEvents(element))
103 return true;
104
105 return false;
106}
107
108TouchEventHandler::TouchEventHandler(WebPagePrivate* webpage)
109 : m_webPage(webpage)
110 , m_didCancelTouch(false)
111 , m_convertTouchToMouse(false)
112 , m_existingTouchMode(ProcessedTouchEvents)
113{
114}
115
116TouchEventHandler::~TouchEventHandler()
117{
118}
119
120bool TouchEventHandler::shouldSuppressMouseDownOnTouchDown() const
121{
122 return m_lastFatFingersResult.isTextInput() || m_webPage->m_inputHandler->isInputMode() || m_webPage->m_selectionHandler->isSelectionActive();
123}
124
125void TouchEventHandler::touchEventCancel()
126{
127 m_webPage->m_inputHandler->processPendingClientNavigationModeChangeNotification();
128
129 if (!shouldSuppressMouseDownOnTouchDown()) {
130 // Input elements delay mouse down and do not need to be released on touch cancel.
131 m_webPage->m_page->focusController()->focusedOrMainFrame()->eventHandler()->setMousePressed(false);
132 }
133 m_convertTouchToMouse = false;
134 m_didCancelTouch = true;
135
136 // If we cancel a single touch event, we need to also clean up any hover
137 // state we get into by synthetically moving the mouse to the m_fingerPoint.
138 Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable();
139 if (elementUnderFatFinger && elementUnderFatFinger->renderer()) {
140
141 HitTestRequest request(HitTestRequest::FingerUp);
142 // The HitTestResult point is not actually needed.
143 HitTestResult result(IntPoint::zero());
144 result.setInnerNode(elementUnderFatFinger);
145
146 Document* document = elementUnderFatFinger->document();
147 ASSERT(document);
148 document->renderView()->layer()->updateHoverActiveState(request, result);
149 document->updateStyleIfNeeded();
150 // Updating the document style may destroy the renderer.
151 if (elementUnderFatFinger->renderer())
152 elementUnderFatFinger->renderer()->repaint();
153 ASSERT(!elementUnderFatFinger->hovered());
154 }
155
156 m_lastFatFingersResult.reset();
157}
158
159void TouchEventHandler::touchEventCancelAndClearFocusedNode()
160{
161 touchEventCancel();
162 m_webPage->clearFocusNode();
163}
164
165void TouchEventHandler::touchHoldEvent()
166{
167 // This is a hack for our hack that converts the touch pressed event that we've delayed because the user has focused a input field
168 // to the page as a mouse pressed event.
169 if (shouldSuppressMouseDownOnTouchDown())
170 handleFatFingerPressed();
171
172 // Clear the focus ring indication if tap-and-hold'ing on a link.
173 if (m_lastFatFingersResult.validNode() && m_lastFatFingersResult.validNode()->isLink())
174 m_webPage->clearFocusNode();
175}
176
177bool TouchEventHandler::handleTouchPoint(BlackBerry::Platform::TouchPoint& point)
178{
179 switch (point.m_state) {
180 case BlackBerry::Platform::TouchPoint::TouchPressed:
181 {
182 m_lastFatFingersResult.reset(); // Theoretically this shouldn't be required. Keep it just in case states get mangled.
183 m_didCancelTouch = false;
184 m_lastScreenPoint = point.m_screenPos;
185
186 IntPoint contentPos(m_webPage->mapFromViewportToContents(point.m_pos));
187
188 m_lastFatFingersResult = FatFingers(m_webPage, contentPos, FatFingers::ClickableElement).findBestPoint();
189
190 Element* elementUnderFatFinger = 0;
191 if (m_lastFatFingersResult.positionWasAdjusted() && m_lastFatFingersResult.validNode()) {
192 ASSERT(m_lastFatFingersResult.validNode()->isElementNode());
193 elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable();
194 }
195
196 // Set or reset the touch mode.
197 Element* possibleTargetNodeForMouseMoveEvents = static_cast<Element*>(m_lastFatFingersResult.positionWasAdjusted() ? elementUnderFatFinger : m_lastFatFingersResult.validNode());
198 m_convertTouchToMouse = shouldConvertTouchToMouse(possibleTargetNodeForMouseMoveEvents);
199
200 if (elementUnderFatFinger)
201 drawTapHighlight();
202
203 // Lets be conservative here: since we have problems on major website having
204 // mousemove listener for no good reason (e.g. google.com, desktop edition),
205 // let only delay client notifications when there is not input text node involved.
206 if (m_convertTouchToMouse
207 && (m_webPage->m_inputHandler->isInputMode() && !m_lastFatFingersResult.isTextInput())) {
208 m_webPage->m_inputHandler->setDelayClientNotificationOfNavigationModeChange(true);
209 handleFatFingerPressed();
210 } else if (!shouldSuppressMouseDownOnTouchDown())
211 handleFatFingerPressed();
212
213 return true;
214 }
215 case BlackBerry::Platform::TouchPoint::TouchReleased:
216 {
217 m_webPage->m_inputHandler->processPendingClientNavigationModeChangeNotification();
218
219 if (shouldSuppressMouseDownOnTouchDown())
220 handleFatFingerPressed();
221
222 // The rebase has eliminated a necessary event when the mouse does not
223 // trigger an actual selection change preventing re-showing of the
224 // keyboard. If input mode is active, call setNavigationMode which
225 // will update the state and display keyboard if needed.
226 if (m_webPage->m_inputHandler->isInputMode())
227 m_webPage->m_inputHandler->setNavigationMode(true);
228
229 IntPoint adjustedPoint;
230 if (m_convertTouchToMouse) {
231 adjustedPoint = point.m_pos;
232 m_convertTouchToMouse = false;
233 } else // Fat finger point in viewport coordinates.
234 adjustedPoint = m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition());
235
236 // Create MouseReleased Event.
237 PlatformMouseEvent mouseEvent(adjustedPoint, m_lastScreenPoint, MouseEventReleased, 1, LeftButton, TouchScreen);
238 m_webPage->handleMouseEvent(mouseEvent);
239 m_lastFatFingersResult.reset(); // reset the fat finger result as its no longer valid when a user's finger is not on the screen.
240
241 unsigned int spellLength = spellCheck(point);
242 if (spellLength) {
243 unsigned int end = m_webPage->m_inputHandler->caretPosition();
244 unsigned int start = end - spellLength;
245 m_webPage->m_client->requestSpellingSuggestionsForString(start, end);
246 }
247 return true;
248 }
249 case BlackBerry::Platform::TouchPoint::TouchMoved:
250 if (m_convertTouchToMouse) {
251 PlatformMouseEvent mouseEvent(point.m_pos, m_lastScreenPoint, MouseEventMoved, 1, LeftButton, TouchScreen);
252 m_lastScreenPoint = point.m_screenPos;
253 if (!m_webPage->handleMouseEvent(mouseEvent)) {
254 m_convertTouchToMouse = false;
255 return false;
256 }
257 return true;
258 }
259 break;
260 default:
261 break;
262 }
263 return false;
264}
265
266unsigned TouchEventHandler::spellCheck(BlackBerry::Platform::TouchPoint& touchPoint)
267{
268 Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable();
269 if (!m_lastFatFingersResult.isTextInput() || !elementUnderFatFinger)
270 return 0;
271
272 IntPoint contentPos(m_webPage->mapFromViewportToContents(touchPoint.m_pos));
273 contentPos = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), m_webPage->focusedOrMainFrame(), contentPos);
274
275 Document* document = elementUnderFatFinger->document();
276 ASSERT(document);
277 RenderedDocumentMarker* marker = document->markers()->renderedMarkerContainingPoint(contentPos, DocumentMarker::Spelling);
278 if (!marker)
279 return 0;
280
281 IntRect rect = marker->renderedRect();
282 IntPoint newContentPos = IntPoint(rect.x() + rect.width(), rect.y() + rect.height() / 2); // midway of right edge
283 Frame* frame = m_webPage->focusedOrMainFrame();
284 if (frame != m_webPage->mainFrame())
285 newContentPos = m_webPage->mainFrame()->view()->windowToContents(frame->view()->contentsToWindow(newContentPos));
286 m_lastFatFingersResult.m_adjustedPosition = newContentPos;
287 m_lastFatFingersResult.m_positionWasAdjusted = true;
288 return marker->endOffset() - marker->startOffset();
289}
290
291void TouchEventHandler::handleFatFingerPressed()
292{
293 if (!m_didCancelTouch) {
294
295 // Convert touch event to a mouse event
296 // First update the mouse position with a MouseMoved event
297 // Send the mouse move event
298 PlatformMouseEvent mouseMoveEvent(m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition()), m_lastScreenPoint, MouseEventMoved, 0, LeftButton, TouchScreen);
299 m_webPage->handleMouseEvent(mouseMoveEvent);
300
301 // Then send the MousePressed event
302 PlatformMouseEvent mousePressedEvent(m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition()), m_lastScreenPoint, MouseEventPressed, 1, LeftButton, TouchScreen);
303 m_webPage->handleMouseEvent(mousePressedEvent);
304 }
305}
306
307// This method filters what element will get tap-highlight'ed or not. To start with,
308// we are going to highlight links (anchors with a valid href element), and elements
309// whose tap highlight color value is different than the default value.
310static Element* elementForTapHighlight(Element* elementUnderFatFinger)
311{
312 bool isArea = elementUnderFatFinger->hasTagName(HTMLNames::areaTag);
313
314 // Do not bail out right way here if there element does not have a renderer. It is the case
315 // for <map> (descendent of <area>) elements. The associated <image> element actually has the
316 // renderer.
317 if (elementUnderFatFinger->renderer()) {
318 Color tapHighlightColor = elementUnderFatFinger->renderStyle()->tapHighlightColor();
319 if (tapHighlightColor != RenderTheme::defaultTheme()->platformTapHighlightColor())
320 return elementUnderFatFinger;
321 }
322
323 Node* linkNode = elementUnderFatFinger->enclosingLinkEventParentOrSelf();
324 if (!linkNode || !linkNode->isHTMLElement() || (!linkNode->renderer() && !isArea))
325 return 0;
326
327 ASSERT(linkNode->isLink());
328
329 // FatFingers class selector ensure only anchor with valid href attr value get here.
330 // It includes empty hrefs.
331 Element* highlightCandidateElement = static_cast<Element*>(linkNode);
332
333 if (!isArea)
334 return highlightCandidateElement;
335
336 HTMLAreaElement* area = static_cast<HTMLAreaElement*>(highlightCandidateElement);
337 HTMLImageElement* image = area->imageElement();
338 if (image && image->renderer())
339 return image;
340
341 return 0;
342}
343
344void TouchEventHandler::drawTapHighlight()
345{
346 Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable();
347 if (!elementUnderFatFinger)
348 return;
349
350 Element* element = elementForTapHighlight(elementUnderFatFinger);
351 if (!element)
352 return;
353
354 // Get the element bounding rect in transformed coordinates so we can extract
355 // the focus ring relative position each rect.
356 RenderObject* renderer = element->renderer();
357 ASSERT(renderer);
358
359 Frame* elementFrame = element->document()->frame();
360 ASSERT(elementFrame);
361
362 FrameView* elementFrameView = elementFrame->view();
363 if (!elementFrameView)
364 return;
365
366 // Tell the client if the element is either in a scrollable container or in a fixed positioned container.
367 // On the client side, this info is being used to hide the tap highlight window on scroll.
368 RenderLayer* layer = m_webPage->enclosingFixedPositionedAncestorOrSelfIfFixedPositioned(renderer->enclosingLayer());
369 bool shouldHideTapHighlightRightAfterScrolling = !layer->renderer()->isRenderView();
370 shouldHideTapHighlightRightAfterScrolling |= !!m_webPage->m_inRegionScrollStartingNode.get();
371
372 IntPoint framePos(m_webPage->frameOffset(elementFrame));
373
374 // FIXME: We can get more precise on the MAP case by calculating the rect with HTMLAreaElement::computeRect().
375 IntRect absoluteRect = renderer->absoluteClippedOverflowRect();
376 absoluteRect.move(framePos.x(), framePos.y());
377
378 IntRect clippingRect;
379 if (elementFrame == m_webPage->mainFrame())
380 clippingRect = IntRect(IntPoint(0, 0), elementFrameView->contentsSize());
381 else
382 clippingRect = m_webPage->mainFrame()->view()->windowToContents(m_webPage->getRecursiveVisibleWindowRect(elementFrameView, true /*noClipToMainFrame*/));
383 clippingRect = intersection(absoluteRect, clippingRect);
384
385 Vector<FloatQuad> focusRingQuads;
386 renderer->absoluteFocusRingQuads(focusRingQuads);
387
388 Platform::IntRectRegion region;
389 for (size_t i = 0; i < focusRingQuads.size(); ++i) {
390 IntRect rect = focusRingQuads[i].enclosingBoundingBox();
391 rect.move(framePos.x(), framePos.y());
392 IntRect clippedRect = intersection(clippingRect, rect);
393 clippedRect.inflate(2);
394 region = unionRegions(region, Platform::IntRect(clippedRect));
395 }
396
397 Color highlightColor = element->renderStyle()->tapHighlightColor();
398
399 m_webPage->m_client->drawTapHighlight(region,
400 highlightColor.red(),
401 highlightColor.green(),
402 highlightColor.blue(),
403 highlightColor.alpha(),
404 shouldHideTapHighlightRightAfterScrolling);
405}
406
407}
408}