/* * Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies) * Copyright (C) 2009 Antonio Gomes * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "SpatialNavigation.h" #include "Frame.h" #include "FrameTree.h" #include "FrameView.h" #include "HTMLFrameOwnerElement.h" #include "IntRect.h" #include "Node.h" #include "Page.h" #include "RenderLayer.h" #include "Settings.h" namespace WebCore { static long long spatialDistance(FocusDirection, const IntRect&, const IntRect&); static IntRect renderRectRelativeToRootDocument(RenderObject*); static RectsAlignment alignmentForRects(FocusDirection, const IntRect&, const IntRect&); static bool areRectsFullyAligned(FocusDirection, const IntRect&, const IntRect&); static bool areRectsPartiallyAligned(FocusDirection, const IntRect&, const IntRect&); static bool isRectInDirection(FocusDirection, const IntRect&, const IntRect&); static void deflateIfOverlapped(IntRect&, IntRect&); static bool checkNegativeCoordsForNode(Node*, const IntRect&); bool isSpatialNavigationEnabled(const Frame* frame) { return (frame && frame->settings() && frame->settings()->isSpatialNavigationEnabled()); } void distanceDataForNode(FocusDirection direction, Node* start, FocusCandidate& candidate) { RenderObject* startRender = start->renderer(); if (!startRender) { candidate.distance = maxDistance(); return; } RenderObject* destRender = candidate.node->renderer(); if (!destRender) { candidate.distance = maxDistance(); return; } IntRect curRect = renderRectRelativeToRootDocument(startRender); IntRect targetRect = renderRectRelativeToRootDocument(destRender); // The bounding rectangle of two consecutive nodes can overlap. In such cases, // deflate both. deflateIfOverlapped(curRect, targetRect); // If empty rects or negative width or height, bail out. if (curRect.isEmpty() || targetRect.isEmpty() || targetRect.width() <= 0 || targetRect.height() <= 0) { candidate.distance = maxDistance(); return; } // Negative coordinates can be used if node is scrolled up offscreen. if (!checkNegativeCoordsForNode(start, curRect)) { candidate.distance = maxDistance(); return; } if (!checkNegativeCoordsForNode(candidate.node, targetRect)) { candidate.distance = maxDistance(); return; } if (!isRectInDirection(direction, curRect, targetRect)) { candidate.distance = maxDistance(); return; } // The distance between two nodes is not to be considered alone when evaluating/looking // for the best focus candidate node. Alignment of rects can be also a good point to be // considered in order to make the algorithm to behavior in a more intuitive way. candidate.alignment = alignmentForRects(direction, curRect, targetRect); candidate.distance = spatialDistance(direction, curRect, targetRect); } // FIXME: This function does not behave correctly with transformed frames. static IntRect renderRectRelativeToRootDocument(RenderObject* render) { ASSERT(render && render->node()); IntRect rect = render->node()->getRect(); // In cases when the |render|'s associated node is in a scrollable inner // document, we only consider its scrollOffset if it is not offscreen. Node* node = render->node(); Document* mainDocument = node->document()->page()->mainFrame()->document(); bool considerScrollOffset = !(hasOffscreenRect(node) && node->document() != mainDocument); if (considerScrollOffset) { if (FrameView* frameView = render->node()->document()->view()) rect.move(-frameView->scrollOffset()); } // Handle nested frames. for (Frame* frame = render->document()->frame(); frame; frame = frame->tree()->parent()) { if (Element* element = static_cast(frame->ownerElement())) { do { rect.move(element->offsetLeft(), element->offsetTop()); } while ((element = element->offsetParent())); } } return rect; } static RectsAlignment alignmentForRects(FocusDirection direction, const IntRect& curRect, const IntRect& targetRect) { if (areRectsFullyAligned(direction, curRect, targetRect)) return Full; if (areRectsPartiallyAligned(direction, curRect, targetRect)) return Partial; return None; } static inline bool isHorizontalMove(FocusDirection direction) { return direction == FocusDirectionLeft || direction == FocusDirectionRight; } static inline int start(FocusDirection direction, const IntRect& rect) { return isHorizontalMove(direction) ? rect.y() : rect.x(); } static inline int middle(FocusDirection direction, const IntRect& rect) { IntPoint center(rect.center()); return isHorizontalMove(direction) ? center.y(): center.x(); } static inline int end(FocusDirection direction, const IntRect& rect) { return isHorizontalMove(direction) ? rect.bottom() : rect.right(); } // This method checks if rects |a| and |b| are fully aligned either vertically or // horizontally. In general, rects whose central point falls between the top or // bottom of each other are considered fully aligned. // Rects that match this criteria are preferable target nodes in move focus changing // operations. // * a = Current focused node's rect. // * b = Focus candidate node's rect. static bool areRectsFullyAligned(FocusDirection direction, const IntRect& a, const IntRect& b) { int aStart, bStart, aEnd, bEnd; switch (direction) { case FocusDirectionLeft: aStart = a.x(); bEnd = b.right(); break; case FocusDirectionRight: aStart = b.x(); bEnd = a.right(); break; case FocusDirectionUp: aStart = a.y(); bEnd = b.y(); break; case FocusDirectionDown: aStart = b.y(); bEnd = a.y(); break; default: ASSERT_NOT_REACHED(); return false; } if (aStart < bEnd) return false; aStart = start(direction, a); bStart = start(direction, b); int aMiddle = middle(direction, a); int bMiddle = middle(direction, b); aEnd = end(direction, a); bEnd = end(direction, b); // Picture of the totally aligned logic: // // Horizontal Vertical Horizontal Vertical // **************************** ***************************** // * _ * _ _ _ _ * * _ * _ _ * // * |_| _ * |_|_|_|_| * * _ |_| * |_|_| * // * |_|....|_| * . * * |_|....|_| * . * // * |_| |_| (1) . * * |_| |_| (2) . * // * |_| * _._ * * |_| * _ _._ _ * // * * |_|_| * * * |_|_|_|_| * // * * * * * * // **************************** ***************************** // Horizontal Vertical Horizontal Vertical // **************************** ***************************** // * _......_ * _ _ _ _ * * _ * _ _ _ _ * // * |_| |_| * |_|_|_|_| * * |_| _ * |_|_|_|_| * // * |_| |_| * . * * |_| |_| * . * // * |_| (3) . * * |_|....|_| (4) . * // * * ._ _ * * * _ _. * // * * |_|_| * * * |_|_| * // * * * * * * // **************************** ***************************** return ((bMiddle >= aStart && bMiddle <= aEnd) // (1) || (aMiddle >= bStart && aMiddle <= bEnd) // (2) || (bStart == aStart) // (3) || (bEnd == aEnd)); // (4) } // This method checks if |start| and |dest| have a partial intersection, either // horizontally or vertically. // * a = Current focused node's rect. // * b = Focus candidate node's rect. static bool areRectsPartiallyAligned(FocusDirection direction, const IntRect& a, const IntRect& b) { int aStart = start(direction, a); int bStart = start(direction, b); int bMiddle = middle(direction, b); int aEnd = end(direction, a); int bEnd = end(direction, b); // Picture of the partially aligned logic: // // Horizontal Vertical // ******************************** // * _ * _ _ _ * // * |_| * |_|_|_| * // * |_|.... _ * . . * // * |_| |_| * . . * // * |_|....|_| * ._._ _ * // * |_| * |_|_|_| * // * |_| * * // * * * // ******************************** // // ... and variants of the above cases. return ((bStart >= aStart && bStart <= aEnd) || (bStart >= aStart && bStart <= aEnd) || (bEnd >= aStart && bEnd <= aEnd) || (bMiddle >= aStart && bMiddle <= aEnd) || (bEnd >= aStart && bEnd <= aEnd)); } // Return true if rect |a| is below |b|. False otherwise. static inline bool below(const IntRect& a, const IntRect& b) { return a.y() > b.bottom(); } // Return true if rect |a| is on the right of |b|. False otherwise. static inline bool rightOf(const IntRect& a, const IntRect& b) { return a.x() > b.right(); } // * a = Current focused node's rect. // * b = Focus candidate node's rect. static long long spatialDistance(FocusDirection direction, const IntRect& a, const IntRect& b) { int x1 = 0, y1 = 0, x2 = 0, y2 = 0; if (direction == FocusDirectionLeft) { // #1 |--| // // #2 |--| |--| // // #3 |--| x1 = a.x(); x2 = b.right(); if (below(a, b)) { // #1 The a rect is below b. y1 = a.y(); y2 = b.bottom(); } else if (below(b, a)) { // #3 The b rect is below a. y1 = a.bottom(); y2 = b.y(); } else { // #2 Both b and a share some common y's. y1 = 0; y2 = 0; } } else if (direction == FocusDirectionRight) { // |--| #1 // // |--| |--| #2 // // |--| #3 x1 = a.right(); x2 = b.x(); if (below(a, b)) { // #1 The b rect is above a. y1 = a.y(); y2 = b.bottom(); } else if (below(b, a)) { // #3 The b rect is below a. y1 = a.bottom(); y2 = b.y(); } else { // #2 Both b and a share some common y's. y1 = 0; y2 = 0; } } else if (direction == FocusDirectionUp) { // // #1 #2 #3 // // |--| |--| |--| // // |--| y1 = a.y(); y2 = b.bottom(); if (rightOf(a, b)) { // #1 The b rect is to the left of a. x1 = a.x(); x2 = b.right(); } else if (rightOf(b, a)) { // #3 The b rect is to the right of a. x1 = a.right(); x2 = b.x(); } else { // #2 Both b and a share some common x's. x1 = 0; x2 = 0; } } else if (direction == FocusDirectionDown) { // |--| // // |--| |--| |--| // // #1 #2 #3 y1 = a.bottom(); y2 = b.y(); if (rightOf(a, b)) { // #1 The b rect is to the left of a. x1 = a.x(); x2 = b.right(); } else if (rightOf(b, a)) { // #3 The b rect is to the right of a x1 = a.right(); x2 = b.x(); } else { // #2 Both b and a share some common x's. x1 = 0; x2 = 0; } } long long dx = x1 - x2; long long dy = y1 - y2; long long distance = (dx * dx) + (dy * dy); if (distance < 0) distance *= -1; return distance; } static bool isRectInDirection(FocusDirection direction, const IntRect& curRect, const IntRect& targetRect) { IntPoint center(targetRect.center()); int targetMiddle = isHorizontalMove(direction) ? center.x() : center.y(); switch (direction) { case FocusDirectionLeft: return targetMiddle < curRect.x(); case FocusDirectionRight: return targetMiddle > curRect.right(); case FocusDirectionUp: return targetMiddle < curRect.y(); case FocusDirectionDown: return targetMiddle > curRect.bottom(); default: ASSERT_NOT_REACHED(); } return false; } // Checks if |node| is offscreen the visible area (viewport) of its container // document. In case it is, one can scroll in direction or take any different // desired action later on. bool hasOffscreenRect(Node* node) { // Get the FrameView in which |node| is (which means the current viewport if |node| // is not in an inner document), so we can check if its content rect is visible // before we actually move the focus to it. FrameView* frameView = node->document()->view(); if (!frameView) return true; IntRect containerViewportRect = frameView->visibleContentRect(); RenderObject* render = node->renderer(); if (!render) return true; IntRect rect(render->absoluteClippedOverflowRect()); if (rect.isEmpty()) return true; return !containerViewportRect.intersects(rect); } // In a bottom-up way, this method tries to scroll |frame| in a given direction // |direction|, going up in the frame tree hierarchy in case it does not succeed. bool scrollInDirection(Frame* frame, FocusDirection direction, const FocusCandidate& candidate) { if (!frame) return false; ScrollDirection scrollDirection; switch (direction) { case FocusDirectionLeft: scrollDirection = ScrollLeft; break; case FocusDirectionRight: scrollDirection = ScrollRight; break; case FocusDirectionUp: scrollDirection = ScrollUp; break; case FocusDirectionDown: scrollDirection = ScrollDown; break; default: return false; } if (!candidate.isNull() && isScrollableContainerNode(candidate.enclosingScrollableBox)) return frame->eventHandler()->scrollRecursively(scrollDirection, ScrollByLine, candidate.enclosingScrollableBox); return frame->eventHandler()->scrollRecursively(scrollDirection, ScrollByLine); } void scrollIntoView(Element* element) { // NOTE: Element's scrollIntoView method could had been used here, but // it is preferable to inflate |element|'s bounding rect a bit before // scrolling it for accurate reason. // Element's scrollIntoView method does not provide this flexibility. IntRect bounds = element->getRect(); bounds.inflate(fudgeFactor()); element->renderer()->enclosingLayer()->scrollRectToVisible(bounds); } bool isInRootDocument(Node* node) { if (!node) return false; Document* rootDocument = node->document()->page()->mainFrame()->document(); return node->document() == rootDocument; } static void deflateIfOverlapped(IntRect& a, IntRect& b) { if (!a.intersects(b) || a.contains(b) || b.contains(a)) return; int deflateFactor = -fudgeFactor(); // Avoid negative width or height values. if ((a.width() + 2 * deflateFactor > 0) && (a.height() + 2 * deflateFactor > 0)) a.inflate(deflateFactor); if ((b.width() + 2 * deflateFactor > 0) && (b.height() + 2 * deflateFactor > 0)) b.inflate(deflateFactor); } static bool checkNegativeCoordsForNode(Node* node, const IntRect& curRect) { ASSERT(node || node->renderer()); if (curRect.x() >= 0 && curRect.y() >= 0) return true; bool canBeScrolled = false; RenderObject* renderer = node->renderer(); for (; renderer; renderer = renderer->parent()) { if (renderer->isBox() && toRenderBox(renderer)->canBeScrolledAndHasScrollableArea()) { canBeScrolled = true; break; } } return canBeScrolled; } bool isScrollableContainerNode(Node* node) { if (!node) return false; if (RenderObject* renderer = node->renderer()) { return (renderer->isBox() && toRenderBox(renderer)->canBeScrolledAndHasScrollableArea() && node->hasChildNodes() && !node->isDocumentNode()); } return false; } bool isNodeDeepDescendantOfDocument(Node* node, Document* baseDocument) { if (!node || !baseDocument) return false; bool descendant = baseDocument == node->document(); Element* currentElement = static_cast(node); while (!descendant) { Element* documentOwner = currentElement->document()->ownerElement(); if (!documentOwner) break; descendant = documentOwner->document() == baseDocument; currentElement = documentOwner; } return descendant; } } // namespace WebCore