|
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/. */ |
|
4 |
|
5 #include "PositionedEventTargeting.h" |
|
6 |
|
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> |
|
19 |
|
20 namespace mozilla { |
|
21 |
|
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 */ |
|
63 |
|
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 }; |
|
72 |
|
73 static EventRadiusPrefs sMouseEventRadiusPrefs; |
|
74 static EventRadiusPrefs sTouchEventRadiusPrefs; |
|
75 |
|
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 } |
|
91 |
|
92 if (!prefs->mRegistered) { |
|
93 prefs->mRegistered = true; |
|
94 |
|
95 nsPrintfCString enabledPref("ui.%s.radius.enabled", prefBranch); |
|
96 Preferences::AddBoolVarCache(&prefs->mEnabled, enabledPref.get(), false); |
|
97 |
|
98 nsPrintfCString visitedWeightPref("ui.%s.radius.visitedWeight", prefBranch); |
|
99 Preferences::AddUintVarCache(&prefs->mVisitedWeight, visitedWeightPref.get(), 100); |
|
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 } |
|
107 |
|
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 } |
|
115 |
|
116 return prefs; |
|
117 } |
|
118 |
|
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 } |
|
127 |
|
128 return false; |
|
129 } |
|
130 |
|
131 static bool gTouchEventsRegistered = false; |
|
132 static int32_t gTouchEventsEnabled = 0; |
|
133 |
|
134 static bool |
|
135 HasTouchListener(nsIContent* aContent) |
|
136 { |
|
137 EventListenerManager* elm = aContent->GetExistingListenerManager(); |
|
138 if (!elm) { |
|
139 return false; |
|
140 } |
|
141 |
|
142 if (!gTouchEventsRegistered) { |
|
143 Preferences::AddIntVarCache(&gTouchEventsEnabled, |
|
144 "dom.w3c_touch_events.enabled", gTouchEventsEnabled); |
|
145 gTouchEventsRegistered = true; |
|
146 } |
|
147 |
|
148 if (!gTouchEventsEnabled) { |
|
149 return false; |
|
150 } |
|
151 |
|
152 return elm->HasListenersFor(nsGkAtoms::ontouchstart) || |
|
153 elm->HasListenersFor(nsGkAtoms::ontouchend); |
|
154 } |
|
155 |
|
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 } |
|
221 |
|
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 } |
|
230 |
|
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 } |
|
243 |
|
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 } |
|
256 |
|
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 } |
|
264 |
|
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 } |
|
280 |
|
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; |
|
288 |
|
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 } |
|
298 |
|
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]; |
|
310 |
|
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); |
|
316 |
|
317 if (region.IsEmpty()) { |
|
318 continue; |
|
319 } |
|
320 |
|
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 } |
|
326 |
|
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 } |
|
338 |
|
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 } |
|
354 |
|
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); |
|
365 |
|
366 const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->eventStructType); |
|
367 if (!prefs || !prefs->mEnabled || (target && IsElementClickable(target, nsGkAtoms::body))) { |
|
368 return target; |
|
369 } |
|
370 |
|
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 } |
|
379 |
|
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; |
|
387 |
|
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 } |
|
395 |
|
396 nsIFrame* closestClickable = |
|
397 GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs, |
|
398 restrictToDescendants, candidates); |
|
399 return closestClickable ? closestClickable : target; |
|
400 } |
|
401 |
|
402 } |