layout/base/PositionedEventTargeting.cpp

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 #include "PositionedEventTargeting.h"
     7 #include "mozilla/EventListenerManager.h"
     8 #include "mozilla/EventStates.h"
     9 #include "mozilla/MouseEvents.h"
    10 #include "mozilla/Preferences.h"
    11 #include "nsLayoutUtils.h"
    12 #include "nsGkAtoms.h"
    13 #include "nsPrintfCString.h"
    14 #include "mozilla/dom/Element.h"
    15 #include "nsRegion.h"
    16 #include "nsDeviceContext.h"
    17 #include "nsIFrame.h"
    18 #include <algorithm>
    20 namespace mozilla {
    22 /*
    23  * The basic goal of FindFrameTargetedByInputEvent() is to find a good
    24  * target element that can respond to mouse events. Both mouse events and touch
    25  * events are targeted at this element. Note that even for touch events, we
    26  * check responsiveness to mouse events. We assume Web authors
    27  * designing for touch events will take their own steps to account for
    28  * inaccurate touch events.
    29  *
    30  * IsElementClickable() encapsulates the heuristic that determines whether an
    31  * element is expected to respond to mouse events. An element is deemed
    32  * "clickable" if it has registered listeners for "click", "mousedown" or
    33  * "mouseup", or is on a whitelist of element tags (<a>, <button>, <input>,
    34  * <select>, <textarea>, <label>), or has role="button", or is a link, or
    35  * is a suitable XUL element.
    36  * Any descendant (in the same document) of a clickable element is also
    37  * deemed clickable since events will propagate to the clickable element from its
    38  * descendant.
    39  *
    40  * If the element directly under the event position is clickable (or
    41  * event radii are disabled), we always use that element. Otherwise we collect
    42  * all frames intersecting a rectangle around the event position (taking CSS
    43  * transforms into account) and choose the best candidate in GetClosest().
    44  * Only IsElementClickable() candidates are considered; if none are found,
    45  * then we revert to targeting the element under the event position.
    46  * We ignore candidates outside the document subtree rooted by the
    47  * document of the element directly under the event position. This ensures that
    48  * event listeners in ancestor documents don't make it completely impossible
    49  * to target a non-clickable element in a child document.
    50  *
    51  * When both a frame and its ancestor are in the candidate list, we ignore
    52  * the ancestor. Otherwise a large ancestor element with a mouse event listener
    53  * and some descendant elements that need to be individually targetable would
    54  * disable intelligent targeting of those descendants within its bounds.
    55  *
    56  * GetClosest() computes the transformed axis-aligned bounds of each
    57  * candidate frame, then computes the Manhattan distance from the event point
    58  * to the bounds rect (which can be zero). The frame with the
    59  * shortest distance is chosen. For visited links we multiply the distance
    60  * by a specified constant weight; this can be used to make visited links
    61  * more or less likely to be targeted than non-visited links.
    62  */
    64 struct EventRadiusPrefs
    65 {
    66   uint32_t mVisitedWeight; // in percent, i.e. default is 100
    67   uint32_t mSideRadii[4]; // TRBL order, in millimetres
    68   bool mEnabled;
    69   bool mRegistered;
    70   bool mTouchOnly;
    71 };
    73 static EventRadiusPrefs sMouseEventRadiusPrefs;
    74 static EventRadiusPrefs sTouchEventRadiusPrefs;
    76 static const EventRadiusPrefs*
    77 GetPrefsFor(nsEventStructType aEventStructType)
    78 {
    79   EventRadiusPrefs* prefs = nullptr;
    80   const char* prefBranch = nullptr;
    81   if (aEventStructType == NS_TOUCH_EVENT) {
    82     prefBranch = "touch";
    83     prefs = &sTouchEventRadiusPrefs;
    84   } else if (aEventStructType == NS_MOUSE_EVENT) {
    85     // Mostly for testing purposes
    86     prefBranch = "mouse";
    87     prefs = &sMouseEventRadiusPrefs;
    88   } else {
    89     return nullptr;
    90   }
    92   if (!prefs->mRegistered) {
    93     prefs->mRegistered = true;
    95     nsPrintfCString enabledPref("ui.%s.radius.enabled", prefBranch);
    96     Preferences::AddBoolVarCache(&prefs->mEnabled, enabledPref.get(), false);
    98     nsPrintfCString visitedWeightPref("ui.%s.radius.visitedWeight", prefBranch);
    99     Preferences::AddUintVarCache(&prefs->mVisitedWeight, visitedWeightPref.get(), 100);
   101     static const char prefNames[4][9] =
   102       { "topmm", "rightmm", "bottommm", "leftmm" };
   103     for (int32_t i = 0; i < 4; ++i) {
   104       nsPrintfCString radiusPref("ui.%s.radius.%s", prefBranch, prefNames[i]);
   105       Preferences::AddUintVarCache(&prefs->mSideRadii[i], radiusPref.get(), 0);
   106     }
   108     if (aEventStructType == NS_MOUSE_EVENT) {
   109       Preferences::AddBoolVarCache(&prefs->mTouchOnly,
   110           "ui.mouse.radius.inputSource.touchOnly", true);
   111     } else {
   112       prefs->mTouchOnly = false;
   113     }
   114   }
   116   return prefs;
   117 }
   119 static bool
   120 HasMouseListener(nsIContent* aContent)
   121 {
   122   if (EventListenerManager* elm = aContent->GetExistingListenerManager()) {
   123     return elm->HasListenersFor(nsGkAtoms::onclick) ||
   124            elm->HasListenersFor(nsGkAtoms::onmousedown) ||
   125            elm->HasListenersFor(nsGkAtoms::onmouseup);
   126   }
   128   return false;
   129 }
   131 static bool gTouchEventsRegistered = false;
   132 static int32_t gTouchEventsEnabled = 0;
   134 static bool
   135 HasTouchListener(nsIContent* aContent)
   136 {
   137   EventListenerManager* elm = aContent->GetExistingListenerManager();
   138   if (!elm) {
   139     return false;
   140   }
   142   if (!gTouchEventsRegistered) {
   143     Preferences::AddIntVarCache(&gTouchEventsEnabled,
   144       "dom.w3c_touch_events.enabled", gTouchEventsEnabled);
   145     gTouchEventsRegistered = true;
   146   }
   148   if (!gTouchEventsEnabled) {
   149     return false;
   150   }
   152   return elm->HasListenersFor(nsGkAtoms::ontouchstart) ||
   153          elm->HasListenersFor(nsGkAtoms::ontouchend);
   154 }
   156 static bool
   157 IsElementClickable(nsIFrame* aFrame, nsIAtom* stopAt = nullptr)
   158 {
   159   // Input events propagate up the content tree so we'll follow the content
   160   // ancestors to look for elements accepting the click.
   161   for (nsIContent* content = aFrame->GetContent(); content;
   162        content = content->GetFlattenedTreeParent()) {
   163     nsIAtom* tag = content->Tag();
   164     if (content->IsHTML() && stopAt && tag == stopAt) {
   165       break;
   166     }
   167     if (HasTouchListener(content) || HasMouseListener(content)) {
   168       return true;
   169     }
   170     if (content->IsHTML()) {
   171       if (tag == nsGkAtoms::button ||
   172           tag == nsGkAtoms::input ||
   173           tag == nsGkAtoms::select ||
   174           tag == nsGkAtoms::textarea ||
   175           tag == nsGkAtoms::label) {
   176         return true;
   177       }
   178       // Bug 921928: we don't have access to the content of remote iframe.
   179       // So fluffing won't go there. We do an optimistic assumption here:
   180       // that the content of the remote iframe needs to be a target.
   181       if (tag == nsGkAtoms::iframe &&
   182           content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozbrowser,
   183                                nsGkAtoms::_true, eIgnoreCase) &&
   184           content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::Remote,
   185                                nsGkAtoms::_true, eIgnoreCase)) {
   186         return true;
   187       }
   188     } else if (content->IsXUL()) {
   189       nsIAtom* tag = content->Tag();
   190       // See nsCSSFrameConstructor::FindXULTagData. This code is not
   191       // really intended to be used with XUL, though.
   192       if (tag == nsGkAtoms::button ||
   193           tag == nsGkAtoms::checkbox ||
   194           tag == nsGkAtoms::radio ||
   195           tag == nsGkAtoms::autorepeatbutton ||
   196           tag == nsGkAtoms::menu ||
   197           tag == nsGkAtoms::menubutton ||
   198           tag == nsGkAtoms::menuitem ||
   199           tag == nsGkAtoms::menulist ||
   200           tag == nsGkAtoms::scrollbarbutton ||
   201           tag == nsGkAtoms::resizer) {
   202         return true;
   203       }
   204     }
   205     static nsIContent::AttrValuesArray clickableRoles[] =
   206       { &nsGkAtoms::button, &nsGkAtoms::key, nullptr };
   207     if (content->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::role,
   208                                  clickableRoles, eIgnoreCase) >= 0) {
   209       return true;
   210     }
   211     if (content->IsEditable()) {
   212       return true;
   213     }
   214     nsCOMPtr<nsIURI> linkURI;
   215     if (content->IsLink(getter_AddRefs(linkURI))) {
   216       return true;
   217     }
   218   }
   219   return false;
   220 }
   222 static nscoord
   223 AppUnitsFromMM(nsIFrame* aFrame, uint32_t aMM, bool aVertical)
   224 {
   225   nsPresContext* pc = aFrame->PresContext();
   226   float result = float(aMM) *
   227     (pc->DeviceContext()->AppUnitsPerPhysicalInch() / MM_PER_INCH_FLOAT);
   228   return NSToCoordRound(result);
   229 }
   231 /**
   232  * Clip aRect with the bounds of aFrame in the coordinate system of
   233  * aRootFrame. aRootFrame is an ancestor of aFrame.
   234  */
   235 static nsRect
   236 ClipToFrame(nsIFrame* aRootFrame, nsIFrame* aFrame, nsRect& aRect)
   237 {
   238   nsRect bound = nsLayoutUtils::TransformFrameRectToAncestor(
   239     aFrame, nsRect(nsPoint(0, 0), aFrame->GetSize()), aRootFrame);
   240   nsRect result = bound.Intersect(aRect);
   241   return result;
   242 }
   244 static nsRect
   245 GetTargetRect(nsIFrame* aRootFrame, const nsPoint& aPointRelativeToRootFrame,
   246               nsIFrame* aRestrictToDescendants, const EventRadiusPrefs* aPrefs)
   247 {
   248   nsMargin m(AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[0], true),
   249              AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[1], false),
   250              AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[2], true),
   251              AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[3], false));
   252   nsRect r(aPointRelativeToRootFrame, nsSize(0,0));
   253   r.Inflate(m);
   254   return ClipToFrame(aRootFrame, aRestrictToDescendants, r);
   255 }
   257 static float
   258 ComputeDistanceFromRect(const nsPoint& aPoint, const nsRect& aRect)
   259 {
   260   nscoord dx = std::max(0, std::max(aRect.x - aPoint.x, aPoint.x - aRect.XMost()));
   261   nscoord dy = std::max(0, std::max(aRect.y - aPoint.y, aPoint.y - aRect.YMost()));
   262   return float(NS_hypot(dx, dy));
   263 }
   265 static float
   266 ComputeDistanceFromRegion(const nsPoint& aPoint, const nsRegion& aRegion)
   267 {
   268   MOZ_ASSERT(!aRegion.IsEmpty(), "can't compute distance between point and empty region");
   269   nsRegionRectIterator iter(aRegion);
   270   const nsRect* r;
   271   float minDist = -1;
   272   while ((r = iter.Next()) != nullptr) {
   273     float dist = ComputeDistanceFromRect(aPoint, *r);
   274     if (dist < minDist || minDist < 0) {
   275       minDist = dist;
   276     }
   277   }
   278   return minDist;
   279 }
   281 // Subtract aRegion from aExposedRegion as long as that doesn't make the
   282 // exposed region get too complex or removes a big chunk of the exposed region.
   283 static void
   284 SubtractFromExposedRegion(nsRegion* aExposedRegion, const nsRegion& aRegion)
   285 {
   286   if (aRegion.IsEmpty())
   287     return;
   289   nsRegion tmp;
   290   tmp.Sub(*aExposedRegion, aRegion);
   291   // Don't let *aExposedRegion get too complex, but don't let it fluff out to
   292   // its bounds either. Do let aExposedRegion get more complex if by doing so
   293   // we reduce its area by at least half.
   294   if (tmp.GetNumRects() <= 15 || tmp.Area() <= aExposedRegion->Area()/2) {
   295     *aExposedRegion = tmp;
   296   }
   297 }
   299 static nsIFrame*
   300 GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame,
   301            const nsRect& aTargetRect, const EventRadiusPrefs* aPrefs,
   302            nsIFrame* aRestrictToDescendants, nsTArray<nsIFrame*>& aCandidates)
   303 {
   304   nsIFrame* bestTarget = nullptr;
   305   // Lower is better; distance is in appunits
   306   float bestDistance = 1e6f;
   307   nsRegion exposedRegion(aTargetRect);
   308   for (uint32_t i = 0; i < aCandidates.Length(); ++i) {
   309     nsIFrame* f = aCandidates[i];
   311     bool preservesAxisAlignedRectangles = false;
   312     nsRect borderBox = nsLayoutUtils::TransformFrameRectToAncestor(f,
   313         nsRect(nsPoint(0, 0), f->GetSize()), aRoot, &preservesAxisAlignedRectangles);
   314     nsRegion region;
   315     region.And(exposedRegion, borderBox);
   317     if (region.IsEmpty()) {
   318       continue;
   319     }
   321     if (preservesAxisAlignedRectangles) {
   322       // Subtract from the exposed region if we have a transform that won't make
   323       // the bounds include a bunch of area that we don't actually cover.
   324       SubtractFromExposedRegion(&exposedRegion, region);
   325     }
   327     if (!IsElementClickable(f)) {
   328       continue;
   329     }
   330     // If our current closest frame is a descendant of 'f', skip 'f' (prefer
   331     // the nested frame).
   332     if (bestTarget && nsLayoutUtils::IsProperAncestorFrameCrossDoc(f, bestTarget, aRoot)) {
   333       continue;
   334     }
   335     if (!nsLayoutUtils::IsAncestorFrameCrossDoc(aRestrictToDescendants, f, aRoot)) {
   336       continue;
   337     }
   339     // distance is in appunits
   340     float distance = ComputeDistanceFromRegion(aPointRelativeToRootFrame, region);
   341     nsIContent* content = f->GetContent();
   342     if (content && content->IsElement() &&
   343         content->AsElement()->State().HasState(
   344                                         EventStates(NS_EVENT_STATE_VISITED))) {
   345       distance *= aPrefs->mVisitedWeight / 100.0f;
   346     }
   347     if (distance < bestDistance) {
   348       bestDistance = distance;
   349       bestTarget = f;
   350     }
   351   }
   352   return bestTarget;
   353 }
   355 nsIFrame*
   356 FindFrameTargetedByInputEvent(const WidgetGUIEvent* aEvent,
   357                               nsIFrame* aRootFrame,
   358                               const nsPoint& aPointRelativeToRootFrame,
   359                               uint32_t aFlags)
   360 {
   361   uint32_t flags = (aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME) ?
   362      nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME : 0;
   363   nsIFrame* target =
   364     nsLayoutUtils::GetFrameForPoint(aRootFrame, aPointRelativeToRootFrame, flags);
   366   const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->eventStructType);
   367   if (!prefs || !prefs->mEnabled || (target && IsElementClickable(target, nsGkAtoms::body))) {
   368     return target;
   369   }
   371   // Do not modify targeting for actual mouse hardware; only for mouse
   372   // events generated by touch-screen hardware.
   373   if (aEvent->eventStructType == NS_MOUSE_EVENT &&
   374       prefs->mTouchOnly &&
   375       aEvent->AsMouseEvent()->inputSource !=
   376         nsIDOMMouseEvent::MOZ_SOURCE_TOUCH) {
   377     return target;
   378   }
   380   // If the exact target is non-null, only consider candidate targets in the same
   381   // document as the exact target. Otherwise, if an ancestor document has
   382   // a mouse event handler for example, targets that are !IsElementClickable can
   383   // never be targeted --- something nsSubDocumentFrame in an ancestor document
   384   // would be targeted instead.
   385   nsIFrame* restrictToDescendants = target ?
   386     target->PresContext()->PresShell()->GetRootFrame() : aRootFrame;
   388   nsRect targetRect = GetTargetRect(aRootFrame, aPointRelativeToRootFrame,
   389                                     restrictToDescendants, prefs);
   390   nsAutoTArray<nsIFrame*,8> candidates;
   391   nsresult rv = nsLayoutUtils::GetFramesForArea(aRootFrame, targetRect, candidates, flags);
   392   if (NS_FAILED(rv)) {
   393     return target;
   394   }
   396   nsIFrame* closestClickable =
   397     GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs,
   398                restrictToDescendants, candidates);
   399   return closestClickable ? closestClickable : target;
   400 }
   402 }

mercurial