| /* |
| * Copyright (C) 2010, 2011, 2012 Research In Motion Limited. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| #include "config.h" |
| #include "TouchEventHandler.h" |
| |
| #include "BlackBerryPlatformSystemSound.h" |
| #include "DOMSupport.h" |
| #include "Document.h" |
| #include "DocumentMarkerController.h" |
| #include "FocusController.h" |
| #include "Frame.h" |
| #include "FrameView.h" |
| #include "HTMLAnchorElement.h" |
| #include "HTMLAreaElement.h" |
| #include "HTMLImageElement.h" |
| #include "HTMLInputElement.h" |
| #include "HTMLNames.h" |
| #include "HTMLPlugInElement.h" |
| #include "InRegionScroller_p.h" |
| #include "InputHandler.h" |
| #include "IntRect.h" |
| #include "IntSize.h" |
| #include "Node.h" |
| #include "Page.h" |
| #include "PlatformMouseEvent.h" |
| #include "PlatformTouchEvent.h" |
| #include "RenderLayer.h" |
| #include "RenderTheme.h" |
| #include "RenderView.h" |
| #include "SelectionHandler.h" |
| #include "WebPage_p.h" |
| #include "WebTapHighlight.h" |
| |
| #include <wtf/MathExtras.h> |
| |
| using namespace WebCore; |
| using namespace WTF; |
| |
| namespace BlackBerry { |
| namespace WebKit { |
| |
| static bool hasMouseMoveListener(Element* element) |
| { |
| ASSERT(element); |
| return element->hasEventListeners(eventNames().mousemoveEvent) || element->document()->hasEventListeners(eventNames().mousemoveEvent); |
| } |
| |
| static bool hasTouchListener(Element* element) |
| { |
| ASSERT(element); |
| return element->hasEventListeners(eventNames().touchstartEvent) |
| || element->hasEventListeners(eventNames().touchmoveEvent) |
| || element->hasEventListeners(eventNames().touchcancelEvent) |
| || element->hasEventListeners(eventNames().touchendEvent); |
| } |
| |
| static bool isRangeControlElement(Element* element) |
| { |
| ASSERT(element); |
| if (!element->hasTagName(HTMLNames::inputTag)) |
| return false; |
| |
| HTMLInputElement* inputElement = static_cast<HTMLInputElement*>(element); |
| return inputElement->isRangeControl(); |
| } |
| |
| static bool shouldConvertTouchToMouse(Element* element) |
| { |
| if (!element) |
| return false; |
| |
| if ((element->hasTagName(HTMLNames::objectTag) || element->hasTagName(HTMLNames::embedTag)) && static_cast<HTMLPlugInElement*>(element)) |
| return true; |
| |
| // Input Range element is a special case that requires natural mouse events |
| // in order to allow dragging of the slider handle. |
| // Input Range element might appear in the webpage, or it might appear in the shadow tree, |
| // like the timeline and volume media controls all use Input Range. |
| // won't operate on the shadow node of other element type, because the webpages |
| // aren't able to attach listeners to shadow content. |
| do { |
| if (isRangeControlElement(element)) |
| return true; |
| element = toElement(element->shadowAncestorNode()); // If an element is not in shadow tree, shadowAncestorNode returns itself. |
| } while (element->isInShadowTree()); |
| |
| // Check if the element has a mouse listener and no touch listener. If so, |
| // the field will require touch events be converted to mouse events to function properly. |
| return hasMouseMoveListener(element) && !hasTouchListener(element); |
| |
| } |
| |
| TouchEventHandler::TouchEventHandler(WebPagePrivate* webpage) |
| : m_webPage(webpage) |
| , m_didCancelTouch(false) |
| , m_convertTouchToMouse(false) |
| , m_existingTouchMode(ProcessedTouchEvents) |
| { |
| } |
| |
| TouchEventHandler::~TouchEventHandler() |
| { |
| } |
| |
| bool TouchEventHandler::shouldSuppressMouseDownOnTouchDown() const |
| { |
| return m_lastFatFingersResult.isTextInput() || m_webPage->m_inputHandler->isInputMode() || m_webPage->m_selectionHandler->isSelectionActive(); |
| } |
| |
| void TouchEventHandler::touchEventCancel() |
| { |
| m_webPage->m_inputHandler->processPendingKeyboardVisibilityChange(); |
| |
| // Input elements delay mouse down and do not need to be released on touch cancel. |
| if (!shouldSuppressMouseDownOnTouchDown()) |
| m_webPage->m_page->focusController()->focusedOrMainFrame()->eventHandler()->setMousePressed(false); |
| |
| m_convertTouchToMouse = m_webPage->m_touchEventMode == PureTouchEventsWithMouseConversion; |
| m_didCancelTouch = true; |
| |
| // If we cancel a single touch event, we need to also clean up any hover |
| // state we get into by synthetically moving the mouse to the m_fingerPoint. |
| Element* elementUnderFatFinger = m_lastFatFingersResult.positionWasAdjusted() ? m_lastFatFingersResult.nodeAsElementIfApplicable() : 0; |
| do { |
| if (!elementUnderFatFinger || !elementUnderFatFinger->renderer()) |
| break; |
| |
| if (!elementUnderFatFinger->renderer()->style()->affectedByHoverRules() |
| && !elementUnderFatFinger->renderer()->style()->affectedByActiveRules()) |
| break; |
| |
| HitTestRequest request(HitTestRequest::TouchEvent | HitTestRequest::Release); |
| // The HitTestResult point is not actually needed. |
| HitTestResult result(IntPoint::zero()); |
| result.setInnerNode(elementUnderFatFinger); |
| |
| Document* document = elementUnderFatFinger->document(); |
| ASSERT(document); |
| document->renderView()->layer()->updateHoverActiveState(request, result); |
| document->updateStyleIfNeeded(); |
| |
| // Updating the document style may destroy the renderer. |
| if (!elementUnderFatFinger->renderer()) |
| break; |
| |
| elementUnderFatFinger->renderer()->repaint(); |
| ASSERT(!elementUnderFatFinger->hovered()); |
| } while (0); |
| |
| m_lastFatFingersResult.reset(); |
| } |
| |
| void TouchEventHandler::touchHoldEvent() |
| { |
| // 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 |
| // to the page as a mouse pressed event. |
| if (shouldSuppressMouseDownOnTouchDown()) |
| handleFatFingerPressed(); |
| |
| // Clear the focus ring indication if tap-and-hold'ing on a link. |
| if (m_lastFatFingersResult.node() && m_lastFatFingersResult.node()->isLink()) |
| m_webPage->clearFocusNode(); |
| } |
| |
| static bool isMainFrameScrollable(const WebPagePrivate* page) |
| { |
| return page->viewportSize().width() < page->contentsSize().width() || page->viewportSize().height() < page->contentsSize().height(); |
| } |
| |
| |
| void TouchEventHandler::playSoundIfAnchorIsTarget() const |
| { |
| if (m_lastFatFingersResult.node() && m_lastFatFingersResult.node()->isLink()) |
| BlackBerry::Platform::SystemSound::instance()->playSound(BlackBerry::Platform::SystemSoundType::InputKeypress); |
| } |
| |
| bool TouchEventHandler::handleTouchPoint(Platform::TouchPoint& point, bool useFatFingers) |
| { |
| // Enable input mode on any touch event. |
| m_webPage->m_inputHandler->setInputModeEnabled(); |
| bool pureWithMouseConversion = m_webPage->m_touchEventMode == PureTouchEventsWithMouseConversion; |
| bool alwaysEnableMouseConversion = pureWithMouseConversion || (!isMainFrameScrollable(m_webPage) && !m_webPage->m_inRegionScroller->d->isActive()); |
| |
| switch (point.m_state) { |
| case Platform::TouchPoint::TouchPressed: |
| { |
| // FIXME: bypass FatFingers if useFatFingers is false |
| m_lastFatFingersResult.reset(); // Theoretically this shouldn't be required. Keep it just in case states get mangled. |
| m_didCancelTouch = false; |
| m_lastScreenPoint = point.m_screenPos; |
| |
| IntPoint contentPos(m_webPage->mapFromViewportToContents(point.m_pos)); |
| |
| m_webPage->postponeDocumentStyleRecalc(); |
| m_lastFatFingersResult = FatFingers(m_webPage, contentPos, FatFingers::ClickableElement).findBestPoint(); |
| |
| Element* elementUnderFatFinger = 0; |
| if (m_lastFatFingersResult.positionWasAdjusted() && m_lastFatFingersResult.node()) { |
| ASSERT(m_lastFatFingersResult.node()->isElementNode()); |
| elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable(); |
| } |
| |
| // Set or reset the touch mode. |
| Element* possibleTargetNodeForMouseMoveEvents = static_cast<Element*>(m_lastFatFingersResult.positionWasAdjusted() ? elementUnderFatFinger : m_lastFatFingersResult.node()); |
| m_convertTouchToMouse = alwaysEnableMouseConversion ? true : shouldConvertTouchToMouse(possibleTargetNodeForMouseMoveEvents); |
| |
| if (!possibleTargetNodeForMouseMoveEvents || (!possibleTargetNodeForMouseMoveEvents->hasEventListeners(eventNames().touchmoveEvent) && !m_convertTouchToMouse)) |
| m_webPage->client()->notifyNoMouseMoveOrTouchMoveHandlers(); |
| |
| m_webPage->resumeDocumentStyleRecalc(); |
| |
| if (elementUnderFatFinger) |
| drawTapHighlight(); |
| |
| // Lets be conservative here: since we have problems on major website having |
| // mousemove listener for no good reason (e.g. google.com, desktop edition), |
| // let only delay client notifications when there is not input text node involved. |
| if (m_convertTouchToMouse |
| && (m_webPage->m_inputHandler->isInputMode() && !m_lastFatFingersResult.isTextInput())) { |
| m_webPage->m_inputHandler->setDelayKeyboardVisibilityChange(true); |
| handleFatFingerPressed(); |
| } else if (!shouldSuppressMouseDownOnTouchDown()) |
| handleFatFingerPressed(); |
| |
| return true; |
| } |
| case Platform::TouchPoint::TouchReleased: |
| { |
| imf_sp_text_t spellCheckOptionRequest; |
| bool shouldRequestSpellCheckOptions = false; |
| |
| if (m_lastFatFingersResult.isTextInput()) |
| shouldRequestSpellCheckOptions = m_webPage->m_inputHandler->shouldRequestSpellCheckingOptionsForPoint(point.m_pos |
| , m_lastFatFingersResult.nodeAsElementIfApplicable(FatFingersResult::ShadowContentNotAllowed, true /* shouldUseRootEditableElement */) |
| , spellCheckOptionRequest); |
| |
| // Apply any suppressed changes. This does not eliminate the need |
| // for the show after the handling of fat finger pressed as it may |
| // have triggered a state change. Leave the change suppressed if |
| // we are triggering spell check options. |
| if (!shouldRequestSpellCheckOptions) |
| m_webPage->m_inputHandler->processPendingKeyboardVisibilityChange(); |
| |
| if (shouldSuppressMouseDownOnTouchDown()) |
| handleFatFingerPressed(); |
| |
| // The rebase has eliminated a necessary event when the mouse does not |
| // trigger an actual selection change preventing re-showing of the |
| // keyboard. If input mode is active, call showVirtualKeyboard which |
| // will update the state and display keyboard if needed. |
| if (m_webPage->m_inputHandler->isInputMode()) |
| m_webPage->m_inputHandler->notifyClientOfKeyboardVisibilityChange(true); |
| |
| m_webPage->m_tapHighlight->hide(); |
| |
| IntPoint adjustedPoint; |
| // always use the true touch point if using the meta-tag, otherwise only use it if we sent mouse moves |
| // to the page and its requested. |
| if (pureWithMouseConversion || (m_convertTouchToMouse && !useFatFingers)) { |
| adjustedPoint = point.m_pos; |
| m_convertTouchToMouse = pureWithMouseConversion; |
| } else // Fat finger point in viewport coordinates. |
| adjustedPoint = m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition()); |
| |
| PlatformMouseEvent mouseEvent(adjustedPoint, m_lastScreenPoint, PlatformEvent::MouseReleased, 1, LeftButton, TouchScreen); |
| m_webPage->handleMouseEvent(mouseEvent); |
| m_lastFatFingersResult.reset(); // Reset the fat finger result as its no longer valid when a user's finger is not on the screen. |
| if (shouldRequestSpellCheckOptions) { |
| IntPoint pixelPositionRelativeToViewport = m_webPage->mapToTransformed(adjustedPoint); |
| m_webPage->m_inputHandler->requestSpellingCheckingOptions(spellCheckOptionRequest, IntSize(m_lastScreenPoint - pixelPositionRelativeToViewport)); |
| } |
| return true; |
| } |
| case Platform::TouchPoint::TouchMoved: |
| if (m_convertTouchToMouse) { |
| PlatformMouseEvent mouseEvent(point.m_pos, m_lastScreenPoint, PlatformEvent::MouseMoved, 1, LeftButton, TouchScreen); |
| m_lastScreenPoint = point.m_screenPos; |
| if (!m_webPage->handleMouseEvent(mouseEvent)) { |
| m_convertTouchToMouse = alwaysEnableMouseConversion; |
| return false; |
| } |
| return true; |
| } |
| break; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| void TouchEventHandler::handleFatFingerPressed() |
| { |
| if (!m_didCancelTouch) { |
| // First update the mouse position with a MouseMoved event. |
| PlatformMouseEvent mouseMoveEvent(m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition()), m_lastScreenPoint, PlatformEvent::MouseMoved, 0, LeftButton, TouchScreen); |
| m_webPage->handleMouseEvent(mouseMoveEvent); |
| |
| // Then send the MousePressed event. |
| PlatformMouseEvent mousePressedEvent(m_webPage->mapFromContentsToViewport(m_lastFatFingersResult.adjustedPosition()), m_lastScreenPoint, PlatformEvent::MousePressed, 1, LeftButton, TouchScreen); |
| m_webPage->handleMouseEvent(mousePressedEvent); |
| } |
| } |
| |
| // This method filters what element will get tap-highlight'ed or not. To start with, |
| // we are going to highlight links (anchors with a valid href element), and elements |
| // whose tap highlight color value is different than the default value. |
| static Element* elementForTapHighlight(Element* elementUnderFatFinger) |
| { |
| // Do not bail out right way here if there element does not have a renderer. |
| // It is the casefor <map> (descendent of <area>) elements. The associated <image> |
| // element actually has the renderer. |
| if (elementUnderFatFinger->renderer()) { |
| Color tapHighlightColor = elementUnderFatFinger->renderStyle()->tapHighlightColor(); |
| if (tapHighlightColor != RenderTheme::defaultTheme()->platformTapHighlightColor()) |
| return elementUnderFatFinger; |
| } |
| |
| bool isArea = elementUnderFatFinger->hasTagName(HTMLNames::areaTag); |
| Node* linkNode = elementUnderFatFinger->enclosingLinkEventParentOrSelf(); |
| if (!linkNode || !linkNode->isHTMLElement() || (!linkNode->renderer() && !isArea)) |
| return 0; |
| |
| ASSERT(linkNode->isLink()); |
| |
| // FatFingers class selector ensure only anchor with valid href attr value get here. |
| // It includes empty hrefs. |
| Element* highlightCandidateElement = static_cast<Element*>(linkNode); |
| |
| if (!isArea) |
| return highlightCandidateElement; |
| |
| HTMLAreaElement* area = static_cast<HTMLAreaElement*>(highlightCandidateElement); |
| HTMLImageElement* image = area->imageElement(); |
| if (image && image->renderer()) |
| return image; |
| |
| return 0; |
| } |
| |
| void TouchEventHandler::drawTapHighlight() |
| { |
| Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable(); |
| if (!elementUnderFatFinger) |
| return; |
| |
| Element* element = elementForTapHighlight(elementUnderFatFinger); |
| if (!element) |
| return; |
| |
| // Get the element bounding rect in transformed coordinates so we can extract |
| // the focus ring relative position each rect. |
| RenderObject* renderer = element->renderer(); |
| ASSERT(renderer); |
| |
| Frame* elementFrame = element->document()->frame(); |
| ASSERT(elementFrame); |
| |
| FrameView* elementFrameView = elementFrame->view(); |
| if (!elementFrameView) |
| return; |
| |
| // Tell the client if the element is either in a scrollable container or in a fixed positioned container. |
| // On the client side, this info is being used to hide the tap highlight window on scroll. |
| RenderLayer* layer = m_webPage->enclosingFixedPositionedAncestorOrSelfIfFixedPositioned(renderer->enclosingLayer()); |
| bool shouldHideTapHighlightRightAfterScrolling = !layer->renderer()->isRenderView(); |
| shouldHideTapHighlightRightAfterScrolling |= !!m_webPage->m_inRegionScroller->d->isActive(); |
| |
| IntPoint framePos(m_webPage->frameOffset(elementFrame)); |
| |
| // FIXME: We can get more precise on the <map> case by calculating the rect with HTMLAreaElement::computeRect(). |
| IntRect absoluteRect(renderer->absoluteClippedOverflowRect()); |
| absoluteRect.move(framePos.x(), framePos.y()); |
| |
| IntRect clippingRect; |
| if (elementFrame == m_webPage->mainFrame()) |
| clippingRect = IntRect(IntPoint(0, 0), elementFrameView->contentsSize()); |
| else |
| clippingRect = m_webPage->mainFrame()->view()->windowToContents(m_webPage->getRecursiveVisibleWindowRect(elementFrameView, true /*noClipToMainFrame*/)); |
| clippingRect = intersection(absoluteRect, clippingRect); |
| |
| Vector<FloatQuad> focusRingQuads; |
| renderer->absoluteFocusRingQuads(focusRingQuads); |
| |
| Platform::IntRectRegion region; |
| for (size_t i = 0; i < focusRingQuads.size(); ++i) { |
| IntRect rect = focusRingQuads[i].enclosingBoundingBox(); |
| rect.move(framePos.x(), framePos.y()); |
| IntRect clippedRect = intersection(clippingRect, rect); |
| clippedRect.inflate(2); |
| region = unionRegions(region, Platform::IntRect(clippedRect)); |
| } |
| |
| Color highlightColor = element->renderStyle()->tapHighlightColor(); |
| |
| m_webPage->m_tapHighlight->draw(region, |
| highlightColor.red(), highlightColor.green(), highlightColor.blue(), highlightColor.alpha(), |
| shouldHideTapHighlightRightAfterScrolling); |
| } |
| |
| } |
| } |