#include "config.h"
#include "FatFingers.h"
#include "BlackBerryPlatformLog.h"
#include "BlackBerryPlatformScreen.h"
#include "BlackBerryPlatformSettings.h"
#include "CSSComputedStyleDeclaration.h"
#include "CSSParser.h"
#include "DOMSupport.h"
#include "Document.h"
#include "Element.h"
#include "EventNames.h"
#include "ExceptionCode.h"
#include "FloatQuad.h"
#include "Frame.h"
#include "FrameView.h"
#include "HTMLFrameOwnerElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLTextAreaElement.h"
#include "Range.h"
#include "RenderObject.h"
#include "RenderView.h"
#include "Text.h"
#include "TextBreakIterator.h"
#include "WebKitThreadViewportAccessor.h"
#include "WebPage_p.h"
#if DEBUG_FAT_FINGERS
#include "BackingStore.h"
#endif
using BlackBerry::Platform::IntRectRegion;
using namespace WebCore;
namespace BlackBerry {
namespace WebKit {
#if DEBUG_FAT_FINGERS
IntRect FatFingers::m_debugFatFingerRect;
IntPoint FatFingers::m_debugFatFingerClickPosition;
IntPoint FatFingers::m_debugFatFingerAdjustedPosition;
#endif
IntRect FatFingers::fingerRectForPoint(const IntPoint& point) const
{
const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
unsigned topPadding, rightPadding, bottomPadding, leftPadding;
IntPoint contentViewportPos = viewportAccessor->documentViewportFromContents(point);
getAdjustedPaddings(contentViewportPos, topPadding, rightPadding, bottomPadding, leftPadding);
return HitTestLocation::rectForPoint(point, topPadding, rightPadding, bottomPadding, leftPadding);
}
static bool hasMousePressListener(Element* element)
{
ASSERT(element);
return element->hasEventListeners(eventNames().clickEvent)
|| element->hasEventListeners(eventNames().mousedownEvent)
|| element->hasEventListeners(eventNames().mouseupEvent);
}
bool FatFingers::isElementClickable(Element* element) const
{
ASSERT(element);
ASSERT(m_targetType == ClickableElement);
ExceptionCode ec = 0;
if (element->webkitMatchesSelector("a[href],*:link,*:visited,*[role=button],button,input,select,label[for],area[href],textarea,embed,object", ec)
|| element->isMediaControlElement()
|| element->isContentEditable()
|| (element->hasTagName(HTMLNames::imgTag) && element->parentNode() && element->parentNode()->hasTagName(HTMLNames::aTag)))
return true;
return hasMousePressListener(element)
|| CSSComputedStyleDeclaration::create(element)->getPropertyValue(cssPropertyID("cursor")) == "pointer";
}
static inline bool isFieldWithText(Node* node)
{
ASSERT(node);
if (!node || !node->isElementNode())
return false;
Element* element = toElement(node);
return !DOMSupport::inputElementText(element).isEmpty();
}
static inline int distanceBetweenPoints(const IntPoint& p1, const IntPoint& p2)
{
int dx = p1.x() - p2.x();
int dy = p1.y() - p2.y();
return sqrt((double)((dx * dx) + (dy * dy)));
}
static bool compareDistanceBetweenPoints(const Platform::IntPoint& p, const IntRectRegion& r1, const IntRectRegion& r2)
{
return distanceBetweenPoints(p, r1.extents().center()) > distanceBetweenPoints(p, r2.extents().center());
}
static bool isValidFrameOwner(WebCore::Element* element)
{
ASSERT(element);
return element->isFrameOwnerElement() && static_cast<HTMLFrameOwnerElement*>(element)->contentFrame();
}
FatFingers::FatFingers(WebPagePrivate* webPage, const WebCore::IntPoint& contentPos, TargetType targetType)
: m_webPage(webPage)
, m_contentPos(contentPos)
, m_targetType(targetType)
{
ASSERT(webPage);
#if DEBUG_FAT_FINGERS
const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
m_debugFatFingerRect = IntRect(0, 0, 0, 0);
m_debugFatFingerClickPosition = viewportAccessor->pixelViewportFromContents(viewportAccessor->roundToPixelFromDocumentContents(WebCore::FloatPoint(contentPos)));
m_debugFatFingerAdjustedPosition = m_debugFatFingerClickPosition;
#endif
}
FatFingers::~FatFingers()
{
}
const FatFingersResult FatFingers::findBestPoint()
{
ASSERT(m_webPage);
ASSERT(m_webPage->m_mainFrame);
IntRect viewportRect = m_webPage->mainFrame()->view()->visibleContentRect();
m_contentPos = Platform::pointClampedToRect(m_contentPos, viewportRect);
FatFingersResult result(m_contentPos);
const HitTestResult& hitResult = m_webPage->hitTestResult(m_contentPos);
Node* node = hitResult.innerNode();
while (node && !node->isElementNode())
node = node->parentNode();
Element* elementUnderPoint = toElement(node);
if (elementUnderPoint) {
result.m_nodeUnderFatFinger = elementUnderPoint;
if (m_targetType == ClickableElement) {
if (isElementClickable(elementUnderPoint)) {
setSuccessfulFatFingersResult(result, elementUnderPoint, m_contentPos );
return result;
}
if (hitResult.URLElement()) {
setSuccessfulFatFingersResult(result, hitResult.URLElement(), m_contentPos );
return result;
}
}
}
#if DEBUG_FAT_FINGERS
if (!m_debugFatFingerRect.isEmpty())
m_webPage->m_backingStore->repaint(0, 0, m_webPage->transformedViewportSize().width(), m_webPage->transformedViewportSize().height(), true, true);
#endif
Vector<IntersectingRegion> intersectingRegions;
IntRectRegion remainingFingerRegion = IntRectRegion(fingerRectForPoint(m_contentPos));
bool foundOne = findIntersectingRegions(m_webPage->m_mainFrame->document(), intersectingRegions, remainingFingerRegion);
if (!foundOne)
return result;
Node* bestNode = 0;
IntRectRegion largestIntersectionRegion;
int largestIntersectionRegionArea = 0;
Vector<IntersectingRegion>::const_iterator endIt = intersectingRegions.end();
for (Vector<IntersectingRegion>::const_iterator it = intersectingRegions.begin(); it != endIt; ++it) {
Node* currentNode = it->first;
IntRectRegion currentIntersectionRegion = it->second;
int currentIntersectionRegionArea = currentIntersectionRegion.area();
if (currentIntersectionRegionArea > largestIntersectionRegionArea
|| (currentIntersectionRegionArea == largestIntersectionRegionArea
&& compareDistanceBetweenPoints(m_contentPos, currentIntersectionRegion, largestIntersectionRegion))) {
bestNode = currentNode;
largestIntersectionRegion = currentIntersectionRegion;
largestIntersectionRegionArea = currentIntersectionRegionArea;
}
}
if (!bestNode || largestIntersectionRegion.isEmpty())
return result;
#if DEBUG_FAT_FINGERS
const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
m_debugFatFingerAdjustedPosition = viewportAccessor->pixelViewportFromContents(
viewportAccessor->roundToPixelFromDocumentContents(largestIntersectionRegion.rects()[0].center()));
#endif
setSuccessfulFatFingersResult(result, bestNode, largestIntersectionRegion.rects()[0].center() );
return result;
}
bool FatFingers::checkFingerIntersection(const IntRectRegion& region, const IntRectRegion& remainingFingerRegion, Node* node, Vector<IntersectingRegion>& intersectingRegions)
{
ASSERT(node);
IntRectRegion regionCopy(region);
WebCore::IntPoint framePos(m_webPage->frameOffset(node->document()->frame()));
regionCopy.move(framePos.x(), framePos.y());
IntRectRegion intersection = intersectRegions(regionCopy, remainingFingerRegion);
if (intersection.isEmpty())
return false;
#if DEBUG_FAT_FINGERS
String nodeName;
if (node->isTextNode())
nodeName = "text node";
else if (node->isElementNode())
nodeName = String::format("%s node", toElement(node)->tagName().latin1().data());
else
nodeName = "unknown node";
if (node->isInShadowTree()) {
nodeName = nodeName + "(in shadow tree";
if (node->isElementNode() && !toElement(node)->shadowPseudoId().isEmpty())
nodeName = nodeName + ", pseudo id " + toElement(node)->shadowPseudoId();
nodeName = nodeName + ")";
}
Platform::logAlways(Platform::LogLevelInfo,
"%s has region %s, intersecting at %s (area %d)", nodeName.latin1().data(),
regionCopy.toString().c_str(), intersection.toString().c_str(), intersection.area());
#endif
intersectingRegions.append(std::make_pair(node, intersection));
return true;
}
bool FatFingers::findIntersectingRegions(Document* document, Vector<IntersectingRegion>& intersectingRegions, IntRectRegion& remainingFingerRegion)
{
if (!document || !document->frame()->view())
return false;
document->updateLayoutIgnorePendingStylesheets();
IntPoint frameContentPos(document->frame()->view()->windowToContents(m_webPage->m_mainFrame->view()->contentsToWindow(m_contentPos)));
IntRect viewportRect = m_webPage->mainFrame()->view()->visibleContentRect();
frameContentPos = Platform::pointClampedToRect(frameContentPos, viewportRect);
#if DEBUG_FAT_FINGERS
const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
Platform::IntRect fingerRect(fingerRectForPoint(frameContentPos));
Platform::IntRect screenFingerRect = viewportAccessor->roundToPixelFromDocumentContents(fingerRect);
Platform::logAlways(Platform::LogLevelInfo, "fat finger rect now %s", screenFingerRect.toString().c_str());
if (document == m_webPage->m_mainFrame->document())
m_debugFatFingerRect = viewportAccessor->pixelViewportFromContents(screenFingerRect);
#endif
bool foundOne = false;
RenderLayer* lowestPositionedEnclosingLayerSoFar = 0;
ListHashSet<RefPtr<Node> > intersectedNodes;
if (m_webPage->m_cachedRectHitTestResults.contains(document))
intersectedNodes = m_webPage->m_cachedRectHitTestResults.get(document);
else
getNodesFromRect(document, frameContentPos, intersectedNodes);
ListHashSet<RefPtr<Node> >::const_iterator it = intersectedNodes.begin();
ListHashSet<RefPtr<Node> >::const_iterator end = intersectedNodes.end();
for ( ; it != end; ++it) {
Node* curNode = (*it).get();
if (!curNode || !curNode->renderer())
continue;
if (remainingFingerRegion.isEmpty())
break;
bool isElement = curNode->isElementNode();
if (isElement && isValidFrameOwner(toElement(curNode))) {
HTMLFrameOwnerElement* owner = static_cast<HTMLFrameOwnerElement*>(curNode);
Document* childDocument = owner && owner->contentFrame() ? owner->contentFrame()->document() : 0;
if (!childDocument)
continue;
ASSERT(childDocument->frame()->view());
foundOne |= findIntersectingRegions(childDocument, intersectingRegions, remainingFingerRegion);
} else if (isElement && m_targetType == ClickableElement) {
foundOne |= checkForClickableElement(toElement(curNode), intersectingRegions, remainingFingerRegion, lowestPositionedEnclosingLayerSoFar);
} else if (m_targetType == Text)
foundOne |= checkForText(curNode, intersectingRegions, remainingFingerRegion);
}
return foundOne;
}
bool FatFingers::checkForClickableElement(Element* curElement, Vector<IntersectingRegion>& intersectingRegions, IntRectRegion& remainingFingerRegion, RenderLayer*& lowestPositionedEnclosingLayerSoFar)
{
ASSERT(curElement);
bool intersects = false;
IntRectRegion elementRegion;
bool isClickableElement = isElementClickable(curElement);
if (isClickableElement) {
if (curElement->isLink()) {
Vector<FloatQuad> quads;
curElement->renderer()->absoluteFocusRingQuads(quads);
size_t n = quads.size();
ASSERT(n);
for (size_t i = 0; i < n; ++i)
elementRegion = unionRegions(elementRegion, Platform::IntRect(quads[i].enclosingBoundingBox()));
} else
elementRegion = IntRectRegion(curElement->renderer()->absoluteBoundingBoxRect(true ));
} else
elementRegion = IntRectRegion(curElement->renderer()->absoluteBoundingBoxRect(true ));
if (lowestPositionedEnclosingLayerSoFar) {
RenderLayer* curElementRenderLayer = m_webPage->enclosingPositionedAncestorOrSelfIfPositioned(curElement->renderer()->enclosingLayer());
if (curElementRenderLayer != lowestPositionedEnclosingLayerSoFar) {
WebCore::IntPoint framePos(m_webPage->frameOffset(curElement->document()->frame()));
IntRectRegion layerRegion(Platform::IntRect(lowestPositionedEnclosingLayerSoFar->renderer()->absoluteBoundingBoxRect(true)));
layerRegion.move(framePos.x(), framePos.y());
remainingFingerRegion = intersectRegions(remainingFingerRegion, layerRegion);
lowestPositionedEnclosingLayerSoFar = curElementRenderLayer;
}
} else
lowestPositionedEnclosingLayerSoFar = m_webPage->enclosingPositionedAncestorOrSelfIfPositioned(curElement->renderer()->enclosingLayer());
if (isClickableElement)
intersects = checkFingerIntersection(elementRegion, remainingFingerRegion, curElement, intersectingRegions);
return intersects;
}
bool FatFingers::checkForText(Node* curNode, Vector<IntersectingRegion>& intersectingRegions, IntRectRegion& fingerRegion)
{
ASSERT(curNode);
if (isFieldWithText(curNode)) {
IntRect boundingRect = curNode->renderer()->absoluteBoundingBoxRect(true );
IntRectRegion nodeRegion(boundingRect);
return checkFingerIntersection(nodeRegion, fingerRegion, curNode, intersectingRegions);
}
if (curNode->isTextNode()) {
WebCore::Text* curText = static_cast<WebCore::Text*>(curNode);
String allText = curText->wholeText();
TextBreakIterator* wordIterator = wordBreakIterator(allText.characters(), allText.length());
int lastOffset = textBreakFirst(wordIterator);
if (lastOffset == -1)
return false;
bool foundOne = false;
int offset;
Document* document = curNode->document();
while ((offset = textBreakNext(wordIterator)) != -1) {
RefPtr<Range> range = Range::create(document, curText, lastOffset, curText, offset);
if (!range->text().stripWhiteSpace().isEmpty()) {
#if DEBUG_FAT_FINGERS
Platform::logAlways(Platform::LogLevelInfo, "Checking word '%s'", range->text().latin1().data());
#endif
IntRectRegion rangeRegion(DOMSupport::transformedBoundingBoxForRange(*range));
foundOne |= checkFingerIntersection(rangeRegion, fingerRegion, curNode, intersectingRegions);
}
lastOffset = offset;
}
return foundOne;
}
return false;
}
void FatFingers::getAdjustedPaddings(const IntPoint& contentViewportPos, unsigned& top, unsigned& right, unsigned& bottom, unsigned& left) const
{
static unsigned topPadding = Platform::Settings::instance()->topFatFingerPadding();
static unsigned rightPadding = Platform::Settings::instance()->rightFatFingerPadding();
static unsigned bottomPadding = Platform::Settings::instance()->bottomFatFingerPadding();
static unsigned leftPadding = Platform::Settings::instance()->leftFatFingerPadding();
double currentScale = m_webPage->currentScale();
top = topPadding / currentScale;
right = rightPadding / currentScale;
bottom = bottomPadding / currentScale;
left = leftPadding / currentScale;
IntRect viewportRect = m_webPage->mainFrame()->view()->visibleContentRect();
top = std::min(unsigned(std::max(contentViewportPos.y() - 1, 0)), top);
left = std::min(unsigned(std::max(contentViewportPos.x() - 1, 0)), left);
bottom = std::min(unsigned(std::max(viewportRect.height() - contentViewportPos.y() - 1, 0)), bottom);
right = std::min(unsigned(std::max(viewportRect.width() - contentViewportPos.x() - 1, 0)), right);
}
void FatFingers::getNodesFromRect(Document* document, const IntPoint& contentPos, ListHashSet<RefPtr<Node> >& intersectedNodes)
{
const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
unsigned topPadding, rightPadding, bottomPadding, leftPadding;
IntPoint contentViewportPos = viewportAccessor->documentViewportFromContents(m_contentPos);
getAdjustedPaddings(contentViewportPos, topPadding, rightPadding, bottomPadding, leftPadding);
HitTestRequest::HitTestRequestType requestType = HitTestRequest::ReadOnly | HitTestRequest::Active;
if (m_targetType != Text)
requestType |= HitTestRequest::DisallowShadowContent;
HitTestResult result(contentPos, topPadding, rightPadding, bottomPadding, leftPadding);
document->renderView()->layer()->hitTest(requestType, result);
intersectedNodes = result.rectBasedTestResult();
m_webPage->m_cachedRectHitTestResults.add(document, intersectedNodes);
}
void FatFingers::setSuccessfulFatFingersResult(FatFingersResult& result, Node* bestNode, const WebCore::IntPoint& adjustedPoint)
{
result.m_nodeUnderFatFinger = bestNode;
result.m_adjustedPosition = adjustedPoint;
result.m_positionWasAdjusted = true;
result.m_isValid = true;
bool isTextInputElement = false;
if (m_targetType == ClickableElement) {
ASSERT_WITH_SECURITY_IMPLICATION(bestNode->isElementNode());
Element* bestElement = toElement(bestNode);
isTextInputElement = DOMSupport::isTextInputElement(bestElement);
}
result.m_isTextInput = isTextInputElement;
}
}
}