1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
|
/*
* Copyright (C) 2010, 2011, 2012, 2013 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 "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;
// Lets make the top padding bigger than other directions, since it gets us more
// accurate clicking results.
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()
|| (isHTMLImageElement(element) && element->parentNode() && isHTMLAnchorElement(element->parentNode())))
return true;
return hasMousePressListener(element)
|| CSSComputedStyleDeclaration::create(element)->getPropertyValue(cssPropertyID("cursor")) == "pointer";
}
// FIXME: Handle content editable nodes here too.
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();
}
// NOTE: 'contentPos' is in main frame contents coordinates.
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);
// Even though we have clamped the point in libwebview to viewport, but there might be a rounding difference for viewport rect.
// Clamp position to viewport to ensure we are inside viewport.
IntRect viewportRect = m_webPage->mainFrame()->view()->visibleContentRect();
m_contentPos = Platform::pointClampedToRect(m_contentPos, viewportRect);
FatFingersResult result(m_contentPos);
// Lets set nodeUnderFatFinger to the result of a point based hit test here. If something
// targable is actually found by ::findIntersectingRegions, then we might replace what we just set below later on.
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 we are looking for a Clickable Element and we found one, we can quit early.
if (m_targetType == ClickableElement) {
if (isElementClickable(elementUnderPoint)) {
setSuccessfulFatFingersResult(result, elementUnderPoint, m_contentPos /*adjustedPosition*/);
return result;
}
if (hitResult.URLElement()) {
setSuccessfulFatFingersResult(result, hitResult.URLElement(), m_contentPos /*adjustedPosition*/);
return result;
}
}
}
#if DEBUG_FAT_FINGERS
// Force blit to make the fat fingers rects show up.
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() /*adjustedPosition*/);
return result;
}
// 'region' is in contents coordinates relative to the frame containing 'node'
// 'remainingFingerRegion' and 'intersectingRegions' will always be in main frame contents
// coordinates.
// Thus, before comparing, we need to map the former to main frame contents coordinates.
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;
}
// intersectingRegions and remainingFingerRegion are all in main frame contents coordinates,
// even on recursive calls of ::findIntersectingRegions.
bool FatFingers::findIntersectingRegions(Document* document, Vector<IntersectingRegion>& intersectingRegions, IntRectRegion& remainingFingerRegion)
{
if (!document || !document->frame()->view())
return false;
// The layout needs to be up-to-date to determine if a node is focusable.
document->updateLayoutIgnorePendingStylesheets();
// Create fingerRect.
IntPoint frameContentPos(document->frame()->view()->windowToContents(m_webPage->m_mainFrame->view()->contentsToWindow(m_contentPos)));
IntRect viewportRect = m_webPage->mainFrame()->view()->visibleContentRect();
// Ensure the frameContentPos is inside the viewport.
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());
// only record the first finger rect
if (document == m_webPage->m_mainFrame->document())
m_debugFatFingerRect = viewportAccessor->pixelViewportFromContents(screenFingerRect);
#endif
bool foundOne = false;
RenderLayer* lowestPositionedEnclosingLayerSoFar = 0;
// Iterate over the list of nodes (and subrects of nodes where possible), for each saving the
// intersection of the bounding box with the finger rect.
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()) {
// Links can wrap lines, and in such cases Node::boundingBox() can give us
// not accurate rects, since it unites all InlineBox's rects. In these
// cases, we can process each line of the link separately with our
// intersection rect, getting a more accurate clicking.
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 /*use transforms*/));
} else
elementRegion = IntRectRegion(curElement->renderer()->absoluteBoundingBoxRect(true /*use transforms*/));
if (lowestPositionedEnclosingLayerSoFar) {
RenderLayer* curElementRenderLayer = m_webPage->enclosingPositionedAncestorOrSelfIfPositioned(curElement->renderer()->enclosingLayer());
if (curElementRenderLayer != lowestPositionedEnclosingLayerSoFar) {
// elementRegion will always be in contents coordinates of its container frame. It needs to be
// mapped to main frame contents coordinates in order to intersect the fingerRegion, then.
WebCore::IntPoint framePos(m_webPage->frameOffset(curElement->document()->frame()));
IntRectRegion layerRegion(Platform::IntRect(lowestPositionedEnclosingLayerSoFar->renderer()->absoluteBoundingBoxRect(true/*use transforms*/)));
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)) {
// FIXME: Find all text in the field and find the best word.
// For now, we will just select the whole field.
IntRect boundingRect = curNode->renderer()->absoluteBoundingBoxRect(true /*use transforms*/);
IntRectRegion nodeRegion(boundingRect);
return checkFingerIntersection(nodeRegion, fingerRegion, curNode, intersectingRegions);
}
if (curNode->isTextNode()) {
WebCore::Text* curText = static_cast<WebCore::Text*>(curNode);
String allText = curText->wholeText();
// Iterate through all words, breaking at whitespace, to find the bounding box of each word.
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();
// We clamp the event position inside the viewport. We should not expand the fat finger rect to the edge again.
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);
// Do not allow fat fingers detect anything not visible(ie outside of the viewport)
getAdjustedPaddings(contentViewportPos, topPadding, rightPadding, bottomPadding, leftPadding);
// The user functions checkForText() and findIntersectingRegions() uses the Node.wholeText() to checkFingerIntersection()
// not the text in its shadow tree.
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;
}
}
}
|