Changeset 55543 in webkit


Ignore:
Timestamp:
Mar 4, 2010 12:19:44 PM (14 years ago)
Author:
tonikitoo@webkit.org
Message:

Extend keyboard navigation to allow directional navigation
https://bugs.webkit.org/show_bug.cgi?id=18662

Reviewed by Simon Fraser, Eric Seidel and Darin Adler.
Patch by Antonio Gomes <tonikitoo@webkit.org>
Based on the initial work of Marco Barisione <marco.barisione@collabora.co.uk>

This patch implements the core logic of the 'Spatial Navigation' feature [1].
It improves the accessibility support of WebCore by extending the basic keyboard
navigation currently available (based on Tab forward and backward) with the
addition of a two-dimensional directional navigation by using Left, Right, Up and
Down arrow keys to move to the "nearest" element in the corresponding direction.

Highlights:

  • Feature is turned off by default in Settings. Port specific APIs need to be added for toggling it on/off.
  • Only elements viewed in the current viewport can have focus move to it. If the "nearest" is not in viewport dimensions, then a scroll-in-direction action is performed.
  • The layout tests added run on Qt's DRT only for now (skipped for Mac, Win and Gtk).

Known issues (to be covered in follow-up bugs):

  • Add port specific hooks to each DRT to enable/disable Spatial Navigation.
  • Support for spatial navigation through form elements (<input>, <select>, etc) is be added.
  • Make navigation keys customizable. It currently works with arrows keys only (up, down, right and left).
  • Make it support modifiers (Alt, Ctrl and Shift).

[1] http://en.wikipedia.org/wiki/Spatial_navigation

  • Android.mk:
  • GNUmakefile.am:
  • WebCore.gypi:
  • WebCore.pro:
  • WebCore.vcproj/WebCore.vcproj:
  • page/EventHandler.cpp:

(WebCore::EventHandler::defaultKeyboardEventHandler):
(WebCore::EventHandler::focusDirectionForKey):
(WebCore::EventHandler::defaultArrowEventHandler):

  • page/EventHandler.h:
  • page/FocusController.cpp:

(WebCore::FocusController::advanceFocus):
(WebCore::FocusController::advanceFocusInDocumentOrder):
(WebCore::FocusController::advanceFocusDirectionally):
(WebCore::updateFocusCandidateIfCloser):
(WebCore::FocusController::findFocusableNodeInDirection):
(WebCore::FocusController::deepFindFocusableNodeInDirection):

  • page/FocusController.h:
  • page/FocusDirection.h:

(WebCore::):

  • page/Settings.cpp:

(WebCore::Settings::Settings):
(WebCore::Settings::setSpatialNavigationEnabled):

  • page/Settings.h:

(WebCore::Settings::isSpatialNavigationEnabled):

  • page/SpatialNavigation.cpp: Added.

(WebCore::distanceInDirection):
(WebCore::renderRectRelativeToRootDocument):
(WebCore::alignmentForRects):
(WebCore::isHorizontalMove):
(WebCore::areRectsFullyAligned):
(WebCore::areRectsPartiallyAligned):
(WebCore::spatialDistance):
(WebCore::isRectInDirection):
(WebCore::hasOffscreenRect):
(WebCore::scrollInDirection):
(WebCore::isInRootDocument):
(WebCore::deflateIfOverlapped):

  • page/SpatialNavigation.h: Added.

(WebCore::):
(WebCore::FocusCandidate::FocusCandidate):

Location:
trunk/WebCore
Files:
2 added
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/WebCore/Android.mk

    r55289 r55543  
    352352        page/SecurityOrigin.cpp \
    353353        page/Settings.cpp \
     354        page/SpatialNavigation.cpp \
    354355        page/UserContentURLPattern.cpp \
    355356        page/WindowFeatures.cpp \
  • trunk/WebCore/ChangeLog

    r55542 r55543  
     12010-03-02  Antonio Gomes  <tonikitoo@webkit.org>
     2
     3        Reviewed by Simon Fraser, Eric Seidel and Darin Adler.
     4        Patch by Antonio Gomes <tonikitoo@webkit.org>
     5        Based on the initial work of Marco Barisione <marco.barisione@collabora.co.uk>
     6
     7        Extend keyboard navigation to allow directional navigation
     8        https://bugs.webkit.org/show_bug.cgi?id=18662
     9
     10        This patch implements the core logic of the 'Spatial Navigation' feature [1].
     11        It improves the accessibility support of WebCore by extending the basic keyboard
     12        navigation currently available (based on Tab forward and backward) with the
     13        addition of a two-dimensional directional navigation by using Left, Right, Up and
     14        Down arrow keys to move to the "nearest" element in the corresponding direction.
     15
     16        Highlights:
     17        * Feature is turned off by default in Settings. Port specific APIs need to be added
     18          for toggling it on/off.
     19        * Only elements viewed in the current viewport can have focus move to it. If the
     20          "nearest" is not in viewport dimensions, then a scroll-in-direction action is
     21          performed.
     22
     23        Known issues (to be covered in follow-up bugs):
     24        * Add port specific hooks to each DRT to enable/disable Spatial Navigation.
     25        * Support for spatial navigation through form elements (<input>, <select>, etc)
     26          is be added.
     27        * Make navigation keys customizable. It currently works with arrows keys only
     28          (up, down, right and left).
     29        * Make it support modifiers (Alt, Ctrl and Shift).
     30        * Improve support on scrollable content.
     31
     32        [1] http://en.wikipedia.org/wiki/Spatial_navigation
     33
     34        * Android.mk:
     35        * GNUmakefile.am:
     36        * WebCore.gypi:
     37        * WebCore.pro:
     38        * WebCore.vcproj/WebCore.vcproj:
     39        * page/EventHandler.cpp:
     40        (WebCore::EventHandler::defaultKeyboardEventHandler):
     41        (WebCore::EventHandler::focusDirectionForKey):
     42        (WebCore::EventHandler::defaultArrowEventHandler):
     43        * page/EventHandler.h:
     44        * page/FocusController.cpp:
     45        (WebCore::FocusController::advanceFocus):
     46        (WebCore::FocusController::advanceFocusInDocumentOrder):
     47        (WebCore::FocusController::advanceFocusDirectionally):
     48        (WebCore::updateFocusCandidateIfCloser):
     49        (WebCore::FocusController::findFocusableNodeInDirection):
     50        (WebCore::FocusController::deepFindFocusableNodeInDirection):
     51        * page/FocusController.h:
     52        * page/FocusDirection.h:
     53        (WebCore::):
     54        * page/Settings.cpp:
     55        (WebCore::Settings::Settings):
     56        (WebCore::Settings::setSpatialNavigationEnabled):
     57        * page/Settings.h:
     58        (WebCore::Settings::isSpatialNavigationEnabled):
     59        * page/SpatialNavigation.cpp: Added.
     60        (WebCore::distanceInDirection):
     61        (WebCore::renderRectRelativeToRootDocument):
     62        (WebCore::alignmentForRects):
     63        (WebCore::isHorizontalMove):
     64        (WebCore::areRectsFullyAligned):
     65        (WebCore::areRectsPartiallyAligned):
     66        (WebCore::spatialDistance):
     67        (WebCore::isRectInDirection):
     68        (WebCore::hasOffscreenRect):
     69        (WebCore::scrollInDirection):
     70        (WebCore::isInRootDocument):
     71        (WebCore::deflateIfOverlapped):
     72        * page/SpatialNavigation.h: Added.
     73        (WebCore::):
     74        (WebCore::FocusCandidate::FocusCandidate):
     75
    1762010-03-04  Beth Dakin  <bdakin@apple.com>
    277
  • trunk/WebCore/GNUmakefile.am

    r55510 r55543  
    14211421        WebCore/page/Settings.cpp \
    14221422        WebCore/page/Settings.h \
     1423        WebCore/page/SpatialNavigation.cpp \
     1424        WebCore/page/SpatialNavigation.h \
    14231425        WebCore/page/UserContentURLPattern.cpp \
    14241426        WebCore/page/UserContentURLPattern.h \
  • trunk/WebCore/WebCore.gypi

    r55522 r55543  
    18841884            'page/Settings.cpp',
    18851885            'page/Settings.h',
     1886            'page/SpatialNavigation.h',
     1887            'page/SpatialNavigation.cpp',
    18861888            'page/UserContentURLPattern.cpp',
    18871889            'page/UserContentURLPattern.h',
  • trunk/WebCore/WebCore.pro

    r55520 r55543  
    774774    page/Screen.cpp \
    775775    page/Settings.cpp \
     776    page/SpatialNavigation.cpp \
    776777    page/UserContentURLPattern.cpp \
    777778    page/WindowFeatures.cpp \
     
    14801481    page/SecurityOrigin.h \
    14811482    page/Settings.h \
     1483    page/SpatialNavigation.h \
    14821484    page/WindowFeatures.h \
    14831485    page/WorkerNavigator.h \
  • trunk/WebCore/WebCore.vcproj/WebCore.vcproj

    r55542 r55543  
    2097420974                        </File>
    2097520975                        <File
     20976                                RelativePath="..\page\SpatialNavigation.cpp"
     20977                                >
     20978                        </File>
     20979                        <File
     20980                                RelativePath="..\page\SpatialNavigation.h"
     20981                                >
     20982                        </File>
     20983                        <File
    2097620984                                RelativePath="..\page\UserContentURLPattern.cpp"
    2097720985                                >
  • trunk/WebCore/page/EventHandler.cpp

    r55436 r55543  
    21752175        if (event->keyIdentifier() == "U+0009")
    21762176            defaultTabEventHandler(event);
    2177 
    2178        // provides KB navigation and selection for enhanced accessibility users
    2179        if (AXObjectCache::accessibilityEnhancedUserInterfaceEnabled())
    2180            handleKeyboardSelectionMovement(event);       
     2177        else {
     2178            FocusDirection direction = focusDirectionForKey(event->keyIdentifier());
     2179            if (direction != FocusDirectionNone)
     2180                defaultArrowEventHandler(direction, event);
     2181        }
     2182
     2183        // provides KB navigation and selection for enhanced accessibility users
     2184        if (AXObjectCache::accessibilityEnhancedUserInterfaceEnabled())
     2185            handleKeyboardSelectionMovement(event);
    21812186    }
    21822187    if (event->type() == eventNames().keypressEvent) {
     
    21872192            defaultSpaceEventHandler(event);
    21882193    }
     2194}
     2195
     2196FocusDirection EventHandler::focusDirectionForKey(const AtomicString& keyIdentifier) const
     2197{
     2198    DEFINE_STATIC_LOCAL(AtomicString, Down, ("Down"));
     2199    DEFINE_STATIC_LOCAL(AtomicString, Up, ("Up"));
     2200    DEFINE_STATIC_LOCAL(AtomicString, Left, ("Left"));
     2201    DEFINE_STATIC_LOCAL(AtomicString, Right, ("Right"));
     2202
     2203    FocusDirection retVal = FocusDirectionNone;
     2204
     2205    if (keyIdentifier == Down)
     2206        retVal = FocusDirectionDown;
     2207    else if (keyIdentifier == Up)
     2208        retVal = FocusDirectionUp;
     2209    else if (keyIdentifier == Left)
     2210        retVal = FocusDirectionLeft;
     2211    else if (keyIdentifier == Right)
     2212        retVal = FocusDirectionRight;
     2213
     2214    return retVal;
    21892215}
    21902216
     
    24832509#endif
    24842510
     2511void EventHandler::defaultArrowEventHandler(FocusDirection focusDirection, KeyboardEvent* event)
     2512{
     2513    if (event->ctrlKey() || event->metaKey() || event->altGraphKey() || event->shiftKey())
     2514        return;
     2515
     2516    Page* page = m_frame->page();
     2517    if (!page)
     2518        return;
     2519
     2520    if (!page->settings() || !page->settings()->isSpatialNavigationEnabled())
     2521        return;
     2522
     2523    // Arrows and other possible directional navigation keys can be used in design
     2524    // mode editing.
     2525    if (m_frame->document()->inDesignMode())
     2526        return;
     2527
     2528    if (page->focusController()->advanceFocus(focusDirection, event))
     2529        event->setDefaultHandled();
     2530}
     2531
    24852532void EventHandler::defaultTabEventHandler(KeyboardEvent* event)
    24862533{
  • trunk/WebCore/page/EventHandler.h

    r55436 r55543  
    2828
    2929#include "DragActions.h"
     30#include "FocusDirection.h"
    3031#include "PlatformMouseEvent.h"
    3132#include "ScrollTypes.h"
     
    309310    void defaultSpaceEventHandler(KeyboardEvent*);
    310311    void defaultTabEventHandler(KeyboardEvent*);
     312    void defaultArrowEventHandler(FocusDirection, KeyboardEvent*);
    311313
    312314#if ENABLE(DRAG_SUPPORT)
     
    330332   
    331333    void setFrameWasScrolledByUser();
     334
     335    FocusDirection focusDirectionForKey(const AtomicString&) const;
    332336
    333337    bool capturesDragging() const { return m_capturesDragging; }
  • trunk/WebCore/page/FocusController.cpp

    r55270 r55543  
    5050#include "SelectionController.h"
    5151#include "Settings.h"
     52#include "SpatialNavigation.h"
    5253#include "Widget.h"
    5354#include <wtf/Platform.h>
     
    5657
    5758using namespace HTMLNames;
     59using namespace std;
    5860
    5961static inline void dispatchEventsOnWindowAndFocusedNode(Document* document, bool focused)
     
    160162
    161163bool FocusController::advanceFocus(FocusDirection direction, KeyboardEvent* event, bool initialFocus)
     164{
     165    switch (direction) {
     166    case FocusDirectionForward:
     167    case FocusDirectionBackward:
     168        return advanceFocusInDocumentOrder(direction, event, initialFocus);
     169    case FocusDirectionLeft:
     170    case FocusDirectionRight:
     171    case FocusDirectionUp:
     172    case FocusDirectionDown:
     173        return advanceFocusDirectionally(direction, event);
     174    default:
     175        ASSERT_NOT_REACHED();
     176    }
     177
     178    return false;
     179}
     180
     181bool FocusController::advanceFocusInDocumentOrder(FocusDirection direction, KeyboardEvent* event, bool initialFocus)
    162182{
    163183    Frame* frame = focusedOrMainFrame();
     
    265285}
    266286
     287bool FocusController::advanceFocusDirectionally(FocusDirection direction, KeyboardEvent* event)
     288{
     289    Frame* frame = focusedOrMainFrame();
     290    ASSERT(frame);
     291    Document* focusedDocument = frame->document();
     292    if (!focusedDocument)
     293        return false;
     294
     295    Node* focusedNode = focusedDocument->focusedNode();
     296    if (!focusedNode) {
     297        // Just move to the first focusable node.
     298        FocusDirection tabDirection = (direction == FocusDirectionUp || direction == FocusDirectionLeft) ?
     299                                       FocusDirectionForward : FocusDirectionBackward;
     300        // 'initialFocus' is set to true so the chrome is not focused.
     301        return advanceFocusInDocumentOrder(tabDirection, event, true);
     302    }
     303
     304    // Move up in the chain of nested frames.
     305    frame = frame->tree()->top();
     306
     307    FocusCandidate focusCandidate;
     308    findFocusableNodeInDirection(frame->document(), focusedNode, direction, event, focusCandidate);
     309
     310    Node* node = focusCandidate.node;
     311    if (!node || !node->isElementNode()) {
     312        // FIXME: May need a way to focus a document here.
     313        Frame* frame = focusedOrMainFrame();
     314        scrollInDirection(frame, direction);
     315        return false;
     316    }
     317
     318    // In order to avoid crazy jump between links that are either far away from each other,
     319    // or just not currently visible, lets do a scroll in the given direction and bail out
     320    // if |node| element is not in the viewport.
     321    if (hasOffscreenRect(node)) {
     322        Frame* frame = node->document()->view()->frame();
     323        scrollInDirection(frame, direction);
     324        return true;
     325    }
     326
     327    Document* newDocument = node->document();
     328
     329    if (newDocument != focusedDocument) {
     330        // Focus is going away from the originally focused document, so clear the focused node.
     331        focusedDocument->setFocusedNode(0);
     332    }
     333
     334    if (newDocument)
     335        setFocusedFrame(newDocument->frame());
     336
     337    static_cast<Element*>(node)->focus(false);
     338    return true;
     339}
     340
     341void updateFocusCandidateIfCloser(Node* focusedNode, Node* candidate, long long distance, FocusCandidate& closestFocusCandidate)
     342{
     343    // Bail out if |distance| is bigger than the current closest candidate.
     344    if (distance >= closestFocusCandidate.distance)
     345        return;
     346
     347    // If |focusedNode| and |candidate| are in the same document AND
     348    // current |closestFocusCandidadte| is not in an {i}frame that is
     349    // preferable to get focused.
     350    if (focusedNode->document() == candidate->document()
     351        && distance < closestFocusCandidate.parentDistance) {
     352        closestFocusCandidate.node = candidate;
     353        closestFocusCandidate.distance = distance;
     354        closestFocusCandidate.parentDistance = cMaxDistance;
     355    } else if (focusedNode->document() != candidate->document()) {
     356        // If the |focusedNode| is in an inner document and the |candidate| is
     357        // in a different document, we only consider to change focus if there is
     358        // not another already good focusable candidate in the same document as
     359        // |focusedNode|.
     360        if (!((isInRootDocument(candidate) && !isInRootDocument(focusedNode))
     361            && closestFocusCandidate.node
     362            && focusedNode->document() == closestFocusCandidate.node->document())) {
     363            closestFocusCandidate.node = candidate;
     364            closestFocusCandidate.distance = distance;
     365        }
     366    }
     367}
     368
     369void FocusController::findFocusableNodeInDirection(Document* document, Node* focusedNode, FocusDirection direction, KeyboardEvent* event, FocusCandidate& closestFocusCandidate)
     370{
     371    ASSERT(document);
     372
     373    // Walk all the child nodes and update focusCandidate if we find a nearer node.
     374    for (Node* candidate = document->firstChild(); candidate; candidate = candidate->traverseNextNode()) {
     375        // Inner documents case.
     376        if (candidate->isFrameOwnerElement())
     377            deepFindFocusableNodeInDirection(focusedNode, candidate, direction, event, closestFocusCandidate);
     378        else if (candidate != focusedNode && candidate->isKeyboardFocusable(event)) {
     379            long long distance = distanceInDirection(focusedNode, candidate,
     380                                                     direction, closestFocusCandidate);
     381            updateFocusCandidateIfCloser(focusedNode, candidate, distance, closestFocusCandidate);
     382        }
     383    }
     384}
     385
     386void FocusController::deepFindFocusableNodeInDirection(Node* focusedNode, Node* candidate, FocusDirection direction, KeyboardEvent* event, FocusCandidate& closestFocusCandidate)
     387{
     388    HTMLFrameOwnerElement* owner = static_cast<HTMLFrameOwnerElement*>(candidate);
     389    if (!owner->contentFrame())
     390        return;
     391
     392    Document* innerDocument = owner->contentFrame()->document();
     393    if (!innerDocument)
     394        return;
     395
     396    if (innerDocument == focusedNode->document())
     397        findFocusableNodeInDirection(innerDocument, focusedNode, direction, event, closestFocusCandidate);
     398    else {
     399        // Check if the current {i}frame element itself is a good candidate
     400        // to move focus to. If it is, then we traverse its inner nodes.
     401        // Lets pass a copy of the best candidate, to not get fooled by a
     402        // frame without focusable elements.
     403        FocusCandidate focusCandidateCopy = closestFocusCandidate;
     404        long long distance = distanceInDirection(focusedNode, candidate, direction, focusCandidateCopy);
     405        if (distance < focusCandidateCopy.distance) {
     406            focusCandidateCopy.parentAlignment = focusCandidateCopy.alignment;
     407            focusCandidateCopy.parentDistance = distance;
     408
     409            findFocusableNodeInDirection(innerDocument, focusedNode, direction, event, focusCandidateCopy);
     410
     411            // If we really have an inner closer focus candidate node, take it.
     412            if (closestFocusCandidate.node != focusCandidateCopy.node)
     413                closestFocusCandidate = focusCandidateCopy;
     414        }
     415    }
     416}
     417
    267418static bool relinquishesEditingFocus(Node *node)
    268419{
  • trunk/WebCore/page/FocusController.h

    r54082 r55543  
    3434namespace WebCore {
    3535
     36class Document;
    3637class Frame;
    3738class KeyboardEvent;
    3839class Node;
    3940class Page;
     41struct FocusCandidate;
    4042
    4143class FocusController : public Noncopyable {
     
    5961
    6062private:
     63    bool advanceFocusDirectionally(FocusDirection, KeyboardEvent*);
     64    bool advanceFocusInDocumentOrder(FocusDirection, KeyboardEvent*, bool initialFocus);
     65
     66    void findFocusableNodeInDirection(Document*, Node*, FocusDirection, KeyboardEvent*, FocusCandidate&);
     67    void deepFindFocusableNodeInDirection(Node*, Node*, FocusDirection, KeyboardEvent*, FocusCandidate&);
     68
    6169    Page* m_page;
    6270    RefPtr<Frame> m_focusedFrame;
  • trunk/WebCore/page/FocusDirection.h

    r29663 r55543  
    2929namespace WebCore {
    3030    enum FocusDirection {
    31         FocusDirectionForward = 0,
    32         FocusDirectionBackward
     31        FocusDirectionNone = 0,
     32        FocusDirectionForward,
     33        FocusDirectionBackward,
     34        FocusDirectionUp,
     35        FocusDirectionDown,
     36        FocusDirectionLeft,
     37        FocusDirectionRight
    3338    };
    3439}
  • trunk/WebCore/page/Settings.cpp

    r55504 r55543  
    6767    , m_pluginAllowedRunTime(numeric_limits<unsigned>::max())
    6868    , m_zoomMode(ZoomPage)
     69    , m_isSpatialNavigationEnabled(false)
    6970    , m_isJavaEnabled(false)
    7071    , m_loadsImagesAutomatically(false)
     
    247248}
    248249
     250void Settings::setSpatialNavigationEnabled(bool isSpatialNavigationEnabled)
     251{
     252    m_isSpatialNavigationEnabled = isSpatialNavigationEnabled;
     253}
     254
    249255void Settings::setJavaEnabled(bool isJavaEnabled)
    250256{
  • trunk/WebCore/page/Settings.h

    r55387 r55543  
    120120        bool javaScriptCanOpenWindowsAutomatically() const { return m_javaScriptCanOpenWindowsAutomatically; }
    121121
     122        void setSpatialNavigationEnabled(bool);
     123        bool isSpatialNavigationEnabled() const { return m_isSpatialNavigationEnabled; }
     124
    122125        void setJavaEnabled(bool);
    123126        bool isJavaEnabled() const { return m_isJavaEnabled; }
     
    312315        unsigned m_pluginAllowedRunTime;
    313316        ZoomMode m_zoomMode;
     317        bool m_isSpatialNavigationEnabled : 1;
    314318        bool m_isJavaEnabled : 1;
    315319        bool m_loadsImagesAutomatically : 1;
Note: See TracChangeset for help on using the changeset viewer.