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.

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

mercurial