[chromium] Implement disambiguation popup (a.k.a. Link Preview)
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 30 Aug 2012 04:08:37 +0000 (04:08 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 30 Aug 2012 04:08:37 +0000 (04:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=94182

Patch by Tien-Ren Chen <trchen@chromium.org> on 2012-08-29
Reviewed by Adam Barth.

In this new implementation, we add a new WebViewClient::handleDisambiguationPopup delegate.
The disambiguation sequence will be initiated by the gesture event handler
in WebViewImpl if an ambiguous tap is detected, then
m_client->handleDisambiguationPopup will be called, so the embedder can
decide whether to swallow the touch event and show a popup.

New test: WebFrameTest.DisambiguationPopupTest

* WebKit.gyp:
* features.gypi:
* public/WebInputEvent.h:
(WebGestureEvent):
(WebKit::WebGestureEvent::WebGestureEvent):
* public/WebTouchCandidatesInfo.h: Removed.
* public/WebView.h:
(WebKit):
* public/WebViewClient.h:
(WebKit):
(WebViewClient):
(WebKit::WebViewClient::triggersLinkPreview):
* src/WebInputEvent.cpp:
(SameSizeAsWebGestureEvent):
* src/WebViewImpl.cpp:
(WebKit):
(WebKit::WebViewImpl::handleGestureEventWithLinkPreview):
(WebKit::WebViewImpl::handleGestureEvent):
* src/WebViewImpl.h:
(WebViewImpl):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@127095 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/WebCore.gypi
Source/WebCore/page/TouchDisambiguation.cpp [new file with mode: 0644]
Source/WebCore/page/TouchDisambiguation.h [new file with mode: 0644]
Source/WebKit/chromium/ChangeLog
Source/WebKit/chromium/public/WebViewClient.h
Source/WebKit/chromium/src/WebViewImpl.cpp
Source/WebKit/chromium/tests/WebFrameTest.cpp
Source/WebKit/chromium/tests/data/disambiguation_popup.html [new file with mode: 0644]

index 946a7a0..861f132 100644 (file)
             'page/SuspendableTimer.h',
             'page/TouchAdjustment.cpp',
             'page/TouchAdjustment.h',
+            'page/TouchDisambiguation.cpp',
+            'page/TouchDisambiguation.h',
             'page/UserContentURLPattern.cpp',
             'page/WebKitAnimation.cpp',
             'page/WebKitAnimation.h',
diff --git a/Source/WebCore/page/TouchDisambiguation.cpp b/Source/WebCore/page/TouchDisambiguation.cpp
new file mode 100644 (file)
index 0000000..5efe3db
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * 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.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER 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 "TouchDisambiguation.h"
+
+#include "Document.h"
+#include "Element.h"
+#include "Frame.h"
+#include "FrameView.h"
+#include "HTMLNames.h"
+#include "HitTestResult.h"
+#include <algorithm>
+#include <cmath>
+
+using namespace std;
+
+namespace WebCore {
+
+static IntRect boundingBoxForEventNodes(Node* eventNode)
+{
+    if (!eventNode->document()->view())
+        return IntRect();
+
+    IntRect result;
+    Node* node = eventNode;
+    while (node) {
+        // Skip the whole sub-tree if the node doesn't propagate events.
+        if (node != eventNode && node->willRespondToMouseClickEvents()) {
+            node = node->traverseNextSibling(eventNode);
+            continue;
+        }
+        result.unite(pixelSnappedIntRect(node->getRect()));
+        node = node->traverseNextNode(eventNode);
+    }
+    return eventNode->document()->view()->contentsToWindow(result);
+}
+
+static float scoreTouchTarget(IntPoint touchPoint, int padding, IntRect boundingBox)
+{
+    if (boundingBox.isEmpty())
+        return 0;
+
+    float reciprocalPadding = 1.f / padding;
+    float score = 1;
+
+    IntSize distance = boundingBox.differenceToPoint(touchPoint);
+    score *= max((padding - abs(distance.width())) * reciprocalPadding, 0.f);
+    score *= max((padding - abs(distance.height())) * reciprocalPadding, 0.f);
+
+    return score;
+}
+
+struct TouchTargetData {
+    IntRect windowBoundingBox;
+    float score;
+};
+
+void findGoodTouchTargets(const IntRect& touchBox, Frame* mainFrame, float pageScaleFactor, Vector<IntRect>& goodTargets)
+{
+    goodTargets.clear();
+
+    int touchPointPadding = ceil(max(touchBox.width(), touchBox.height()) * 0.5);
+    // FIXME: Rect-based hit test doesn't transform the touch point size.
+    //        We have to pre-apply page scale factor here.
+    int padding = ceil(touchPointPadding / pageScaleFactor);
+
+    IntPoint touchPoint = touchBox.center();
+    IntPoint contentsPoint = mainFrame->view()->windowToContents(touchPoint);
+
+    HitTestResult result = mainFrame->eventHandler()->hitTestResultAtPoint(contentsPoint, false, false, DontHitTestScrollbars, HitTestRequest::Active | HitTestRequest::ReadOnly, IntSize(padding, padding));
+    const ListHashSet<RefPtr<Node> >& hitResults = result.rectBasedTestResult();
+
+    HashMap<Node*, TouchTargetData> touchTargets;
+    float bestScore = 0;
+    for (ListHashSet<RefPtr<Node> >::const_iterator it = hitResults.begin(); it != hitResults.end(); ++it) {
+        for (Node* node = it->get(); node; node = node->parentNode()) {
+            if (node->isDocumentNode() || node->hasTagName(HTMLNames::htmlTag) || node->hasTagName(HTMLNames::bodyTag))
+                break;
+            if (node->willRespondToMouseClickEvents()) {
+                TouchTargetData& targetData = touchTargets.add(node, TouchTargetData()).iterator->second;
+                targetData.windowBoundingBox = boundingBoxForEventNodes(node);
+                targetData.score = scoreTouchTarget(touchPoint, touchPointPadding, targetData.windowBoundingBox);
+                bestScore = max(bestScore, targetData.score);
+                break;
+            }
+        }
+    }
+
+    for (HashMap<Node*, TouchTargetData>::iterator it = touchTargets.begin(); it != touchTargets.end(); ++it) {
+        // Currently the scoring function uses the overlap area with the fat point as the score.
+        // We ignore the candidates that has less than 1/2 overlap (we consider not really ambiguous enough) than the best candidate to avoid excessive popups.
+        if (it->second.score < bestScore * 0.5)
+            continue;
+        goodTargets.append(it->second.windowBoundingBox);
+    }
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/page/TouchDisambiguation.h b/Source/WebCore/page/TouchDisambiguation.h
new file mode 100644 (file)
index 0000000..5afeb6b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * 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.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER 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.
+ */
+
+#ifndef TouchDisambiguation_h
+#define TouchDisambiguation_h
+
+#include <wtf/Vector.h>
+
+namespace WebCore {
+
+class Frame;
+class IntRect;
+
+void findGoodTouchTargets(const IntRect& touchBox, Frame* mainFrame, float pageScaleFactor, Vector<IntRect>& goodTargets);
+
+} // namespace WebCore
+
+#endif
index e12068b..75feeba 100644 (file)
@@ -1,3 +1,39 @@
+2012-08-29  Tien-Ren Chen  <trchen@chromium.org>
+
+        [chromium] Implement disambiguation popup (a.k.a. Link Preview)
+        https://bugs.webkit.org/show_bug.cgi?id=94182
+
+        Reviewed by Adam Barth.
+
+        In this new implementation, we add a new WebViewClient::handleDisambiguationPopup delegate.
+        The disambiguation sequence will be initiated by the gesture event handler
+        in WebViewImpl if an ambiguous tap is detected, then
+        m_client->handleDisambiguationPopup will be called, so the embedder can
+        decide whether to swallow the touch event and show a popup.
+
+        New test: WebFrameTest.DisambiguationPopupTest
+
+        * WebKit.gyp:
+        * features.gypi:
+        * public/WebInputEvent.h:
+        (WebGestureEvent):
+        (WebKit::WebGestureEvent::WebGestureEvent):
+        * public/WebTouchCandidatesInfo.h: Removed.
+        * public/WebView.h:
+        (WebKit):
+        * public/WebViewClient.h:
+        (WebKit):
+        (WebViewClient):
+        (WebKit::WebViewClient::triggersLinkPreview):
+        * src/WebInputEvent.cpp:
+        (SameSizeAsWebGestureEvent):
+        * src/WebViewImpl.cpp:
+        (WebKit):
+        (WebKit::WebViewImpl::handleGestureEventWithLinkPreview):
+        (WebKit::WebViewImpl::handleGestureEvent):
+        * src/WebViewImpl.h:
+        (WebViewImpl):
+
 2012-08-29  Dominic Mazzoni  <dmazzoni@google.com>
 
         AX: Canvas should have a distinct role
index 904d4a2..058755c 100644 (file)
@@ -62,6 +62,7 @@ class WebFileChooserCompletion;
 class WebFrame;
 class WebGeolocationClient;
 class WebGeolocationService;
+class WebGestureEvent;
 class WebHelperPlugin;
 class WebHitTestResult;
 class WebIconLoadingCompletion;
@@ -71,6 +72,7 @@ class WebKeyboardEvent;
 class WebNode;
 class WebNotificationPresenter;
 class WebRange;
+class WebRect;
 class WebSpeechInputController;
 class WebSpeechInputListener;
 class WebSpeechRecognizer;
@@ -285,6 +287,9 @@ public:
     // unless the view did not need a layout.
     virtual void didUpdateLayout() { }
 
+    // Return true to swallow the input event if the embedder will start a disambiguation popup
+    virtual bool handleDisambiguationPopup(const WebGestureEvent&, const WebVector<WebRect>& targetRects) { return false; }
+
     // Session history -----------------------------------------------------
 
     // Tells the embedder to navigate back or forward in session history by
index 773873a..626cb0b 100644 (file)
 
 #if ENABLE(GESTURE_EVENTS)
 #include "PlatformGestureEvent.h"
+#include "TouchDisambiguation.h"
 #endif
 
 #if OS(WINDOWS)
@@ -707,12 +708,23 @@ bool WebViewImpl::handleGestureEvent(const WebGestureEvent& event)
         if (detectContentOnTouch(WebPoint(event.x, event.y), event.type))
             return true;
 
-        PlatformGestureEventBuilder platformEvent(mainFrameImpl()->frameView(), event);
         RefPtr<WebCore::PopupContainer> selectPopup;
         selectPopup = m_selectPopup;
         hideSelectPopup();
         ASSERT(!m_selectPopup);
+
+        if (!event.boundingBox.isEmpty()) {
+            Vector<IntRect> goodTargets;
+            findGoodTouchTargets(event.boundingBox, mainFrameImpl()->frame(), pageScaleFactor(), goodTargets);
+            // FIXME: replace touch adjustment code when numberOfGoodTargets == 1?
+            // Single candidate case is currently handled by: https://bugs.webkit.org/show_bug.cgi?id=85101
+            if (goodTargets.size() >= 2 && m_client && m_client->handleDisambiguationPopup(event, goodTargets))
+                return true;
+        }
+
+        PlatformGestureEventBuilder platformEvent(mainFrameImpl()->frameView(), event);
         bool gestureHandled = mainFrameImpl()->frame()->eventHandler()->handleGestureEvent(platformEvent);
+
         if (m_selectPopup && m_selectPopup == selectPopup) {
             // That tap triggered a select popup which is the same as the one that
             // was showing before the tap. It means the user tapped the select
@@ -720,6 +732,7 @@ bool WebViewImpl::handleGestureEvent(const WebGestureEvent& event)
             // immediately reopened the select popup. It needs to be closed.
             hideSelectPopup();
         }
+
         return gestureHandled;
     }
     case WebInputEvent::GestureTwoFingerTap:
index 03c201e..4efdc7b 100644 (file)
@@ -1078,4 +1078,72 @@ TEST_F(WebFrameTest, SelectRange)
     webView->close();
 }
 
+class DisambiguationPopupTestWebViewClient : public WebViewClient {
+public:
+    virtual bool handleDisambiguationPopup(const WebGestureEvent&, const WebVector<WebRect>& targetRects) OVERRIDE
+    {
+        EXPECT_GE(targetRects.size(), 2u);
+        m_triggered = true;
+        return true;
+    }
+
+    bool triggered() const { return m_triggered; }
+    void resetTriggered() { m_triggered = false; }
+    bool m_triggered;
+};
+
+static WebGestureEvent fatTap(int x, int y)
+{
+    WebGestureEvent event;
+    event.type = WebInputEvent::GestureTap;
+    event.x = x;
+    event.y = y;
+    event.boundingBox = WebCore::IntRect(x - 25, y - 25, 50, 50);
+    return event;
+}
+
+TEST_F(WebFrameTest, DisambiguationPopupTest)
+{
+    registerMockedHttpURLLoad("disambiguation_popup.html");
+
+    DisambiguationPopupTestWebViewClient client;
+
+    // Make sure we initialize to minimum scale, even if the window size
+    // only becomes available after the load begins.
+    WebViewImpl* webViewImpl = static_cast<WebViewImpl*>(FrameTestHelpers::createWebViewAndLoad(m_baseURL + "disambiguation_popup.html", true, 0, &client));
+    webViewImpl->resize(WebSize(1000, 1000));
+    webViewImpl->layout();
+
+    client.resetTriggered();
+    webViewImpl->handleInputEvent(fatTap(0, 0));
+    EXPECT_FALSE(client.triggered());
+
+    client.resetTriggered();
+    webViewImpl->handleInputEvent(fatTap(200, 115));
+    EXPECT_FALSE(client.triggered());
+
+    for (int i = 0; i <= 46; i++) {
+        client.resetTriggered();
+        webViewImpl->handleInputEvent(fatTap(120, 230 + i * 5));
+
+        int j = i % 10;
+        if (j >= 7 && j <= 9)
+            EXPECT_TRUE(client.triggered());
+        else
+            EXPECT_FALSE(client.triggered());
+    }
+
+    for (int i = 0; i <= 46; i++) {
+        client.resetTriggered();
+        webViewImpl->handleInputEvent(fatTap(10 + i * 5, 590));
+
+        int j = i % 10;
+        if (j >= 7 && j <= 9)
+            EXPECT_TRUE(client.triggered());
+        else
+            EXPECT_FALSE(client.triggered());
+    }
+
+}
+
 } // namespace
diff --git a/Source/WebKit/chromium/tests/data/disambiguation_popup.html b/Source/WebKit/chromium/tests/data/disambiguation_popup.html
new file mode 100644 (file)
index 0000000..b1af285
--- /dev/null
@@ -0,0 +1,30 @@
+<html>
+<head>
+<title>Disambiguation Popup Test</title>
+<style type="text/css">
+.horizontal-link {
+    display:block;
+    width:200px;
+    height:30px;
+    margin:20px;
+    background-color:#ccccff;
+}
+.vertical-link {
+    display:inline-block;
+    width:30px;
+    height:200px;
+    margin:10px;
+    background-color:#ccccff;
+}
+</style>
+</head>
+<body style="margin:0px;">
+<a href="#" class="horizontal-link" style="margin:100px">Link</a>
+<a href="#" class="horizontal-link">Link 1</a>
+<a href="#" class="horizontal-link">Link 2</a>
+<a href="#" class="horizontal-link">Link 3</a>
+<a href="#" class="horizontal-link">Link 4</a>
+<a href="#" class="horizontal-link">Link 5</a>
+<a href="#" class="vertical-link">Link 1</a><a href="#" class="vertical-link">Link 2</a><a href="#" class="vertical-link">Link 3</a><a href="#" class="vertical-link">Link 4</a><a href="#" class="vertical-link">Link 5</a>
+</body>
+</html>