Wed, 31 Dec 2014 06:09:35 +0100
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 }