/src/mozilla-central/layout/base/PositionedEventTargeting.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
4 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
5 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | |
7 | | #include "PositionedEventTargeting.h" |
8 | | |
9 | | #include "mozilla/EventListenerManager.h" |
10 | | #include "mozilla/EventStates.h" |
11 | | #include "mozilla/MouseEvents.h" |
12 | | #include "mozilla/Preferences.h" |
13 | | #include "mozilla/dom/MouseEventBinding.h" |
14 | | #include "nsLayoutUtils.h" |
15 | | #include "nsGkAtoms.h" |
16 | | #include "nsFontMetrics.h" |
17 | | #include "nsPrintfCString.h" |
18 | | #include "mozilla/dom/Element.h" |
19 | | #include "nsRegion.h" |
20 | | #include "nsDeviceContext.h" |
21 | | #include "nsIContentInlines.h" |
22 | | #include "nsIFrame.h" |
23 | | #include <algorithm> |
24 | | #include "LayersLogging.h" |
25 | | |
26 | | using namespace mozilla; |
27 | | using namespace mozilla::dom; |
28 | | |
29 | | // If debugging this code you may wish to enable this logging, and also |
30 | | // uncomment the DumpFrameTree call near the bottom of the file. |
31 | | #define PET_LOG(...) |
32 | | // #define PET_LOG(...) printf_stderr("PET: " __VA_ARGS__); |
33 | | |
34 | | namespace mozilla { |
35 | | |
36 | | /* |
37 | | * The basic goal of FindFrameTargetedByInputEvent() is to find a good |
38 | | * target element that can respond to mouse events. Both mouse events and touch |
39 | | * events are targeted at this element. Note that even for touch events, we |
40 | | * check responsiveness to mouse events. We assume Web authors |
41 | | * designing for touch events will take their own steps to account for |
42 | | * inaccurate touch events. |
43 | | * |
44 | | * GetClickableAncestor() encapsulates the heuristic that determines whether an |
45 | | * element is expected to respond to mouse events. An element is deemed |
46 | | * "clickable" if it has registered listeners for "click", "mousedown" or |
47 | | * "mouseup", or is on a whitelist of element tags (<a>, <button>, <input>, |
48 | | * <select>, <textarea>, <label>), or has role="button", or is a link, or |
49 | | * is a suitable XUL element. |
50 | | * Any descendant (in the same document) of a clickable element is also |
51 | | * deemed clickable since events will propagate to the clickable element from its |
52 | | * descendant. |
53 | | * |
54 | | * If the element directly under the event position is clickable (or |
55 | | * event radii are disabled), we always use that element. Otherwise we collect |
56 | | * all frames intersecting a rectangle around the event position (taking CSS |
57 | | * transforms into account) and choose the best candidate in GetClosest(). |
58 | | * Only GetClickableAncestor() candidates are considered; if none are found, |
59 | | * then we revert to targeting the element under the event position. |
60 | | * We ignore candidates outside the document subtree rooted by the |
61 | | * document of the element directly under the event position. This ensures that |
62 | | * event listeners in ancestor documents don't make it completely impossible |
63 | | * to target a non-clickable element in a child document. |
64 | | * |
65 | | * When both a frame and its ancestor are in the candidate list, we ignore |
66 | | * the ancestor. Otherwise a large ancestor element with a mouse event listener |
67 | | * and some descendant elements that need to be individually targetable would |
68 | | * disable intelligent targeting of those descendants within its bounds. |
69 | | * |
70 | | * GetClosest() computes the transformed axis-aligned bounds of each |
71 | | * candidate frame, then computes the Manhattan distance from the event point |
72 | | * to the bounds rect (which can be zero). The frame with the |
73 | | * shortest distance is chosen. For visited links we multiply the distance |
74 | | * by a specified constant weight; this can be used to make visited links |
75 | | * more or less likely to be targeted than non-visited links. |
76 | | */ |
77 | | |
78 | | struct EventRadiusPrefs |
79 | | { |
80 | | uint32_t mVisitedWeight; // in percent, i.e. default is 100 |
81 | | uint32_t mSideRadii[4]; // TRBL order, in millimetres |
82 | | bool mEnabled; |
83 | | bool mRegistered; |
84 | | bool mTouchOnly; |
85 | | bool mRepositionEventCoords; |
86 | | bool mTouchClusterDetectionEnabled; |
87 | | bool mSimplifiedClusterDetection; |
88 | | uint32_t mLimitReadableSize; |
89 | | uint32_t mKeepLimitSizeForCluster; |
90 | | }; |
91 | | |
92 | | static EventRadiusPrefs sMouseEventRadiusPrefs; |
93 | | static EventRadiusPrefs sTouchEventRadiusPrefs; |
94 | | |
95 | | static const EventRadiusPrefs* |
96 | | GetPrefsFor(EventClassID aEventClassID) |
97 | 0 | { |
98 | 0 | EventRadiusPrefs* prefs = nullptr; |
99 | 0 | const char* prefBranch = nullptr; |
100 | 0 | if (aEventClassID == eTouchEventClass) { |
101 | 0 | prefBranch = "touch"; |
102 | 0 | prefs = &sTouchEventRadiusPrefs; |
103 | 0 | } else if (aEventClassID == eMouseEventClass) { |
104 | 0 | // Mostly for testing purposes |
105 | 0 | prefBranch = "mouse"; |
106 | 0 | prefs = &sMouseEventRadiusPrefs; |
107 | 0 | } else { |
108 | 0 | return nullptr; |
109 | 0 | } |
110 | 0 | |
111 | 0 | if (!prefs->mRegistered) { |
112 | 0 | prefs->mRegistered = true; |
113 | 0 |
|
114 | 0 | nsPrintfCString enabledPref("ui.%s.radius.enabled", prefBranch); |
115 | 0 | Preferences::AddBoolVarCache(&prefs->mEnabled, enabledPref, false); |
116 | 0 |
|
117 | 0 | nsPrintfCString visitedWeightPref("ui.%s.radius.visitedWeight", prefBranch); |
118 | 0 | Preferences::AddUintVarCache(&prefs->mVisitedWeight, visitedWeightPref, 100); |
119 | 0 |
|
120 | 0 | static const char prefNames[4][9] = |
121 | 0 | { "topmm", "rightmm", "bottommm", "leftmm" }; |
122 | 0 | for (int32_t i = 0; i < 4; ++i) { |
123 | 0 | nsPrintfCString radiusPref("ui.%s.radius.%s", prefBranch, prefNames[i]); |
124 | 0 | Preferences::AddUintVarCache(&prefs->mSideRadii[i], radiusPref, 0); |
125 | 0 | } |
126 | 0 |
|
127 | 0 | if (aEventClassID == eMouseEventClass) { |
128 | 0 | Preferences::AddBoolVarCache(&prefs->mTouchOnly, |
129 | 0 | "ui.mouse.radius.inputSource.touchOnly", true); |
130 | 0 | } else { |
131 | 0 | prefs->mTouchOnly = false; |
132 | 0 | } |
133 | 0 |
|
134 | 0 | nsPrintfCString repositionPref("ui.%s.radius.reposition", prefBranch); |
135 | 0 | Preferences::AddBoolVarCache(&prefs->mRepositionEventCoords, repositionPref, false); |
136 | 0 |
|
137 | 0 | // These values were formerly set by ui.zoomedview preferences. |
138 | 0 | prefs->mTouchClusterDetectionEnabled = false; |
139 | 0 | prefs->mSimplifiedClusterDetection = false; |
140 | 0 | prefs->mLimitReadableSize = 8; |
141 | 0 | prefs->mKeepLimitSizeForCluster = 16; |
142 | 0 | } |
143 | 0 |
|
144 | 0 | return prefs; |
145 | 0 | } |
146 | | |
147 | | static bool |
148 | | HasMouseListener(nsIContent* aContent) |
149 | 0 | { |
150 | 0 | if (EventListenerManager* elm = aContent->GetExistingListenerManager()) { |
151 | 0 | return elm->HasListenersFor(nsGkAtoms::onclick) || |
152 | 0 | elm->HasListenersFor(nsGkAtoms::onmousedown) || |
153 | 0 | elm->HasListenersFor(nsGkAtoms::onmouseup); |
154 | 0 | } |
155 | 0 |
|
156 | 0 | return false; |
157 | 0 | } |
158 | | |
159 | | static bool gTouchEventsRegistered = false; |
160 | | static int32_t gTouchEventsEnabled = 0; |
161 | | |
162 | | static bool |
163 | | HasTouchListener(nsIContent* aContent) |
164 | 0 | { |
165 | 0 | EventListenerManager* elm = aContent->GetExistingListenerManager(); |
166 | 0 | if (!elm) { |
167 | 0 | return false; |
168 | 0 | } |
169 | 0 | |
170 | 0 | if (!gTouchEventsRegistered) { |
171 | 0 | Preferences::AddIntVarCache(&gTouchEventsEnabled, |
172 | 0 | "dom.w3c_touch_events.enabled", gTouchEventsEnabled); |
173 | 0 | gTouchEventsRegistered = true; |
174 | 0 | } |
175 | 0 |
|
176 | 0 | if (!gTouchEventsEnabled) { |
177 | 0 | return false; |
178 | 0 | } |
179 | 0 | |
180 | 0 | return elm->HasListenersFor(nsGkAtoms::ontouchstart) || |
181 | 0 | elm->HasListenersFor(nsGkAtoms::ontouchend); |
182 | 0 | } |
183 | | |
184 | | static bool |
185 | | IsDescendant(nsIFrame* aFrame, nsIContent* aAncestor, nsAutoString* aLabelTargetId) |
186 | 0 | { |
187 | 0 | for (nsIContent* content = aFrame->GetContent(); content; |
188 | 0 | content = content->GetFlattenedTreeParent()) { |
189 | 0 | if (aLabelTargetId && content->IsHTMLElement(nsGkAtoms::label)) { |
190 | 0 | content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_for, |
191 | 0 | *aLabelTargetId); |
192 | 0 | } |
193 | 0 | if (content == aAncestor) { |
194 | 0 | return true; |
195 | 0 | } |
196 | 0 | } |
197 | 0 | return false; |
198 | 0 | } |
199 | | |
200 | | static nsIContent* |
201 | | GetClickableAncestor(nsIFrame* aFrame, nsAtom* stopAt = nullptr, nsAutoString* aLabelTargetId = nullptr) |
202 | 0 | { |
203 | 0 | // Input events propagate up the content tree so we'll follow the content |
204 | 0 | // ancestors to look for elements accepting the click. |
205 | 0 | for (nsIContent* content = aFrame->GetContent(); content; |
206 | 0 | content = content->GetFlattenedTreeParent()) { |
207 | 0 | if (stopAt && content->IsHTMLElement(stopAt)) { |
208 | 0 | break; |
209 | 0 | } |
210 | 0 | if (HasTouchListener(content) || HasMouseListener(content)) { |
211 | 0 | return content; |
212 | 0 | } |
213 | 0 | if (content->IsAnyOfHTMLElements(nsGkAtoms::button, |
214 | 0 | nsGkAtoms::input, |
215 | 0 | nsGkAtoms::select, |
216 | 0 | nsGkAtoms::textarea)) { |
217 | 0 | return content; |
218 | 0 | } |
219 | 0 | if (content->IsHTMLElement(nsGkAtoms::label)) { |
220 | 0 | if (aLabelTargetId) { |
221 | 0 | content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_for, |
222 | 0 | *aLabelTargetId); |
223 | 0 | } |
224 | 0 | return content; |
225 | 0 | } |
226 | 0 |
|
227 | 0 | // Bug 921928: we don't have access to the content of remote iframe. |
228 | 0 | // So fluffing won't go there. We do an optimistic assumption here: |
229 | 0 | // that the content of the remote iframe needs to be a target. |
230 | 0 | if (content->IsHTMLElement(nsGkAtoms::iframe) && |
231 | 0 | content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozbrowser, |
232 | 0 | nsGkAtoms::_true, eIgnoreCase) && |
233 | 0 | content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote, |
234 | 0 | nsGkAtoms::_true, eIgnoreCase)) { |
235 | 0 | return content; |
236 | 0 | } |
237 | 0 | |
238 | 0 | // See nsCSSFrameConstructor::FindXULTagData. This code is not |
239 | 0 | // really intended to be used with XUL, though. |
240 | 0 | if (content->IsAnyOfXULElements(nsGkAtoms::button, |
241 | 0 | nsGkAtoms::checkbox, |
242 | 0 | nsGkAtoms::radio, |
243 | 0 | nsGkAtoms::menu, |
244 | 0 | nsGkAtoms::menuitem, |
245 | 0 | nsGkAtoms::menulist, |
246 | 0 | nsGkAtoms::scrollbarbutton, |
247 | 0 | nsGkAtoms::resizer)) { |
248 | 0 | return content; |
249 | 0 | } |
250 | 0 | |
251 | 0 | static Element::AttrValuesArray clickableRoles[] = |
252 | 0 | { &nsGkAtoms::button, &nsGkAtoms::key, nullptr }; |
253 | 0 | if (content->IsElement() && |
254 | 0 | content->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::role, |
255 | 0 | clickableRoles, eIgnoreCase) >= 0) { |
256 | 0 | return content; |
257 | 0 | } |
258 | 0 | if (content->IsEditable()) { |
259 | 0 | return content; |
260 | 0 | } |
261 | 0 | nsCOMPtr<nsIURI> linkURI; |
262 | 0 | if (content->IsLink(getter_AddRefs(linkURI))) { |
263 | 0 | return content; |
264 | 0 | } |
265 | 0 | } |
266 | 0 | return nullptr; |
267 | 0 | } |
268 | | |
269 | | static nscoord |
270 | | AppUnitsFromMM(nsIFrame* aFrame, uint32_t aMM) |
271 | 0 | { |
272 | 0 | nsPresContext* pc = aFrame->PresContext(); |
273 | 0 | nsIPresShell* presShell = pc->PresShell(); |
274 | 0 | float result = float(aMM) * |
275 | 0 | (pc->DeviceContext()->AppUnitsPerPhysicalInch() / MM_PER_INCH_FLOAT); |
276 | 0 | if (presShell->ScaleToResolution()) { |
277 | 0 | result = result / presShell->GetResolution(); |
278 | 0 | } |
279 | 0 | return NSToCoordRound(result); |
280 | 0 | } |
281 | | |
282 | | /** |
283 | | * Clip aRect with the bounds of aFrame in the coordinate system of |
284 | | * aRootFrame. aRootFrame is an ancestor of aFrame. |
285 | | */ |
286 | | static nsRect |
287 | | ClipToFrame(nsIFrame* aRootFrame, nsIFrame* aFrame, nsRect& aRect) |
288 | 0 | { |
289 | 0 | nsRect bound = nsLayoutUtils::TransformFrameRectToAncestor( |
290 | 0 | aFrame, nsRect(nsPoint(0, 0), aFrame->GetSize()), aRootFrame); |
291 | 0 | nsRect result = bound.Intersect(aRect); |
292 | 0 | return result; |
293 | 0 | } |
294 | | |
295 | | static nsRect |
296 | | GetTargetRect(nsIFrame* aRootFrame, const nsPoint& aPointRelativeToRootFrame, |
297 | | nsIFrame* aRestrictToDescendants, const EventRadiusPrefs* aPrefs, |
298 | | uint32_t aFlags) |
299 | 0 | { |
300 | 0 | nsMargin m(AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[0]), |
301 | 0 | AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[1]), |
302 | 0 | AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[2]), |
303 | 0 | AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[3])); |
304 | 0 | nsRect r(aPointRelativeToRootFrame, nsSize(0,0)); |
305 | 0 | r.Inflate(m); |
306 | 0 | if (!(aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME)) { |
307 | 0 | // Don't clip this rect to the root scroll frame if the flag to ignore the |
308 | 0 | // root scroll frame is set. Note that the GetClosest code will still enforce |
309 | 0 | // that the target found is a descendant of aRestrictToDescendants. |
310 | 0 | r = ClipToFrame(aRootFrame, aRestrictToDescendants, r); |
311 | 0 | } |
312 | 0 | return r; |
313 | 0 | } |
314 | | |
315 | | static float |
316 | | ComputeDistanceFromRect(const nsPoint& aPoint, const nsRect& aRect) |
317 | 0 | { |
318 | 0 | nscoord dx = std::max(0, std::max(aRect.x - aPoint.x, aPoint.x - aRect.XMost())); |
319 | 0 | nscoord dy = std::max(0, std::max(aRect.y - aPoint.y, aPoint.y - aRect.YMost())); |
320 | 0 | return float(NS_hypot(dx, dy)); |
321 | 0 | } |
322 | | |
323 | | static float |
324 | | ComputeDistanceFromRegion(const nsPoint& aPoint, const nsRegion& aRegion) |
325 | 0 | { |
326 | 0 | MOZ_ASSERT(!aRegion.IsEmpty(), "can't compute distance between point and empty region"); |
327 | 0 | float minDist = -1; |
328 | 0 | for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { |
329 | 0 | float dist = ComputeDistanceFromRect(aPoint, iter.Get()); |
330 | 0 | if (dist < minDist || minDist < 0) { |
331 | 0 | minDist = dist; |
332 | 0 | } |
333 | 0 | } |
334 | 0 | return minDist; |
335 | 0 | } |
336 | | |
337 | | // Subtract aRegion from aExposedRegion as long as that doesn't make the |
338 | | // exposed region get too complex or removes a big chunk of the exposed region. |
339 | | static void |
340 | | SubtractFromExposedRegion(nsRegion* aExposedRegion, const nsRegion& aRegion) |
341 | 0 | { |
342 | 0 | if (aRegion.IsEmpty()) |
343 | 0 | return; |
344 | 0 | |
345 | 0 | nsRegion tmp; |
346 | 0 | tmp.Sub(*aExposedRegion, aRegion); |
347 | 0 | // Don't let *aExposedRegion get too complex, but don't let it fluff out to |
348 | 0 | // its bounds either. Do let aExposedRegion get more complex if by doing so |
349 | 0 | // we reduce its area by at least half. |
350 | 0 | if (tmp.GetNumRects() <= 15 || tmp.Area() <= aExposedRegion->Area()/2) { |
351 | 0 | *aExposedRegion = tmp; |
352 | 0 | } |
353 | 0 | } |
354 | | |
355 | | // Search in the list of frames aCandidates if the element with the id "aLabelTargetId" |
356 | | // is present. |
357 | | static bool IsElementPresent(nsTArray<nsIFrame*>& aCandidates, const nsAutoString& aLabelTargetId) |
358 | 0 | { |
359 | 0 | for (uint32_t i = 0; i < aCandidates.Length(); ++i) { |
360 | 0 | nsIFrame* f = aCandidates[i]; |
361 | 0 | nsIContent* aContent = f->GetContent(); |
362 | 0 | if (aContent && aContent->IsElement()) { |
363 | 0 | if (aContent->GetID() && aLabelTargetId == nsAtomString(aContent->GetID())) { |
364 | 0 | return true; |
365 | 0 | } |
366 | 0 | } |
367 | 0 | } |
368 | 0 | return false; |
369 | 0 | } |
370 | | |
371 | | static bool |
372 | | IsLargeElement(nsIFrame* aFrame, const EventRadiusPrefs* aPrefs) |
373 | 0 | { |
374 | 0 | uint32_t keepLimitSizeForCluster = aPrefs->mKeepLimitSizeForCluster; |
375 | 0 | nsSize frameSize = aFrame->GetSize(); |
376 | 0 | nsPresContext* pc = aFrame->PresContext(); |
377 | 0 | nsIPresShell* presShell = pc->PresShell(); |
378 | 0 | float cumulativeResolution = presShell->GetCumulativeResolution(); |
379 | 0 | if ((pc->AppUnitsToGfxUnits(frameSize.height) * cumulativeResolution) > keepLimitSizeForCluster && |
380 | 0 | (pc->AppUnitsToGfxUnits(frameSize.width) * cumulativeResolution) > keepLimitSizeForCluster) { |
381 | 0 | return true; |
382 | 0 | } |
383 | 0 | return false; |
384 | 0 | } |
385 | | |
386 | | static nsIFrame* |
387 | | GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame, |
388 | | const nsRect& aTargetRect, const EventRadiusPrefs* aPrefs, |
389 | | nsIFrame* aRestrictToDescendants, nsIContent* aClickableAncestor, |
390 | | nsTArray<nsIFrame*>& aCandidates, int32_t* aElementsInCluster) |
391 | 0 | { |
392 | 0 | std::vector<nsIContent*> mContentsInCluster; // List of content elements in the cluster without duplicate |
393 | 0 | nsIFrame* bestTarget = nullptr; |
394 | 0 | // Lower is better; distance is in appunits |
395 | 0 | float bestDistance = 1e6f; |
396 | 0 | nsRegion exposedRegion(aTargetRect); |
397 | 0 | for (uint32_t i = 0; i < aCandidates.Length(); ++i) { |
398 | 0 | nsIFrame* f = aCandidates[i]; |
399 | 0 | PET_LOG("Checking candidate %p\n", f); |
400 | 0 |
|
401 | 0 | bool preservesAxisAlignedRectangles = false; |
402 | 0 | nsRect borderBox = nsLayoutUtils::TransformFrameRectToAncestor(f, |
403 | 0 | nsRect(nsPoint(0, 0), f->GetSize()), aRoot, &preservesAxisAlignedRectangles); |
404 | 0 | nsRegion region; |
405 | 0 | region.And(exposedRegion, borderBox); |
406 | 0 | if (region.IsEmpty()) { |
407 | 0 | PET_LOG(" candidate %p had empty hit region\n", f); |
408 | 0 | continue; |
409 | 0 | } |
410 | 0 | |
411 | 0 | if (preservesAxisAlignedRectangles) { |
412 | 0 | // Subtract from the exposed region if we have a transform that won't make |
413 | 0 | // the bounds include a bunch of area that we don't actually cover. |
414 | 0 | SubtractFromExposedRegion(&exposedRegion, region); |
415 | 0 | } |
416 | 0 |
|
417 | 0 | nsAutoString labelTargetId; |
418 | 0 | if (aClickableAncestor && !IsDescendant(f, aClickableAncestor, &labelTargetId)) { |
419 | 0 | PET_LOG(" candidate %p is not a descendant of required ancestor\n", f); |
420 | 0 | continue; |
421 | 0 | } |
422 | 0 | |
423 | 0 | nsIContent* clickableContent = GetClickableAncestor(f, nsGkAtoms::body, &labelTargetId); |
424 | 0 | if (!aClickableAncestor && !clickableContent) { |
425 | 0 | PET_LOG(" candidate %p was not clickable\n", f); |
426 | 0 | continue; |
427 | 0 | } |
428 | 0 | // If our current closest frame is a descendant of 'f', skip 'f' (prefer |
429 | 0 | // the nested frame). |
430 | 0 | if (bestTarget && nsLayoutUtils::IsProperAncestorFrameCrossDoc(f, bestTarget, aRoot)) { |
431 | 0 | PET_LOG(" candidate %p was ancestor for bestTarget %p\n", f, bestTarget); |
432 | 0 | continue; |
433 | 0 | } |
434 | 0 | if (!aClickableAncestor && !nsLayoutUtils::IsAncestorFrameCrossDoc(aRestrictToDescendants, f, aRoot)) { |
435 | 0 | PET_LOG(" candidate %p was not descendant of restrictroot %p\n", f, aRestrictToDescendants); |
436 | 0 | continue; |
437 | 0 | } |
438 | 0 | |
439 | 0 | // If the first clickable ancestor of f is a label element |
440 | 0 | // and "for" attribute is present in label element, search the frame list for the "for" element |
441 | 0 | // If this element is present in the current list, do not count the frame in |
442 | 0 | // the cluster elements counter |
443 | 0 | if ((labelTargetId.IsEmpty() || !IsElementPresent(aCandidates, labelTargetId)) && |
444 | 0 | !IsLargeElement(f, aPrefs)) { |
445 | 0 | if (std::find(mContentsInCluster.begin(), mContentsInCluster.end(), clickableContent) == mContentsInCluster.end()) { |
446 | 0 | mContentsInCluster.push_back(clickableContent); |
447 | 0 | } |
448 | 0 | } |
449 | 0 |
|
450 | 0 | // distance is in appunits |
451 | 0 | float distance = ComputeDistanceFromRegion(aPointRelativeToRootFrame, region); |
452 | 0 | nsIContent* content = f->GetContent(); |
453 | 0 | if (content && content->IsElement() && |
454 | 0 | content->AsElement()->State().HasState( |
455 | 0 | EventStates(NS_EVENT_STATE_VISITED))) { |
456 | 0 | distance *= aPrefs->mVisitedWeight / 100.0f; |
457 | 0 | } |
458 | 0 | if (distance < bestDistance) { |
459 | 0 | PET_LOG(" candidate %p is the new best\n", f); |
460 | 0 | bestDistance = distance; |
461 | 0 | bestTarget = f; |
462 | 0 | } |
463 | 0 | } |
464 | 0 | *aElementsInCluster = mContentsInCluster.size(); |
465 | 0 | return bestTarget; |
466 | 0 | } |
467 | | |
468 | | /* |
469 | | * Return always true when touch cluster detection is OFF. |
470 | | * When cluster detection is ON, return true: |
471 | | * if the text inside the frame is readable (by human eyes) |
472 | | * or |
473 | | * if the structure is too complex to determine the size. |
474 | | * In both cases, the frame is considered as clickable. |
475 | | * |
476 | | * Frames with a too small size will return false. |
477 | | * In this case, the frame is considered not clickable. |
478 | | */ |
479 | | static bool |
480 | | IsElementClickableAndReadable(nsIFrame* aFrame, WidgetGUIEvent* aEvent, const EventRadiusPrefs* aPrefs) |
481 | 0 | { |
482 | 0 | if (!aPrefs->mTouchClusterDetectionEnabled) { |
483 | 0 | return true; |
484 | 0 | } |
485 | 0 | |
486 | 0 | if (aPrefs->mSimplifiedClusterDetection) { |
487 | 0 | return true; |
488 | 0 | } |
489 | 0 | |
490 | 0 | if (aEvent->mClass != eMouseEventClass) { |
491 | 0 | return true; |
492 | 0 | } |
493 | 0 | |
494 | 0 | uint32_t limitReadableSize = aPrefs->mLimitReadableSize; |
495 | 0 | nsSize frameSize = aFrame->GetSize(); |
496 | 0 | nsPresContext* pc = aFrame->PresContext(); |
497 | 0 | nsIPresShell* presShell = pc->PresShell(); |
498 | 0 | float cumulativeResolution = presShell->GetCumulativeResolution(); |
499 | 0 | if ((pc->AppUnitsToGfxUnits(frameSize.height) * cumulativeResolution) < limitReadableSize || |
500 | 0 | (pc->AppUnitsToGfxUnits(frameSize.width) * cumulativeResolution) < limitReadableSize) { |
501 | 0 | return false; |
502 | 0 | } |
503 | 0 | // We want to detect small clickable text elements using the font size. |
504 | 0 | // Two common cases are supported for now: |
505 | 0 | // 1. text node |
506 | 0 | // 2. any element with only one child of type text node |
507 | 0 | // All the other cases are currently ignored. |
508 | 0 | nsIContent *content = aFrame->GetContent(); |
509 | 0 | bool testFontSize = false; |
510 | 0 | if (content) { |
511 | 0 | nsINodeList* childNodes = content->ChildNodes(); |
512 | 0 | uint32_t childNodeCount = childNodes->Length(); |
513 | 0 | if ((content->IsText()) || |
514 | 0 | // click occurs on the text inside <a></a> or other clickable tags with text inside |
515 | 0 |
|
516 | 0 | (childNodeCount == 1 && childNodes->Item(0) && |
517 | 0 | childNodes->Item(0)->IsText())) { |
518 | 0 | // The click occurs on an element with only one text node child. In this case, the font size |
519 | 0 | // can be tested. |
520 | 0 | // The number of child nodes is tested to avoid the following cases (See bug 1172488): |
521 | 0 | // Some jscript libraries transform text elements into Canvas elements but keep the text nodes |
522 | 0 | // with a very small size (1px) to handle the selection of text. |
523 | 0 | // With such libraries, the font size of the text elements is not relevant to detect small elements. |
524 | 0 |
|
525 | 0 | testFontSize = true; |
526 | 0 | } |
527 | 0 | } |
528 | 0 |
|
529 | 0 | if (testFontSize) { |
530 | 0 | RefPtr<nsFontMetrics> fm = |
531 | 0 | nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame); |
532 | 0 | if (fm && fm->EmHeight() > 0 && // See bug 1171731 |
533 | 0 | (pc->AppUnitsToGfxUnits(fm->EmHeight()) * cumulativeResolution) < limitReadableSize) { |
534 | 0 | return false; |
535 | 0 | } |
536 | 0 | } |
537 | 0 | |
538 | 0 | return true; |
539 | 0 | } |
540 | | |
541 | | nsIFrame* |
542 | | FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent, |
543 | | nsIFrame* aRootFrame, |
544 | | const nsPoint& aPointRelativeToRootFrame, |
545 | | uint32_t aFlags) |
546 | 0 | { |
547 | 0 | uint32_t flags = (aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME) ? |
548 | 0 | nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME : 0; |
549 | 0 | nsIFrame* target = |
550 | 0 | nsLayoutUtils::GetFrameForPoint(aRootFrame, aPointRelativeToRootFrame, flags); |
551 | 0 | PET_LOG("Found initial target %p for event class %s point %s relative to root frame %p\n", |
552 | 0 | target, (aEvent->mClass == eMouseEventClass ? "mouse" : |
553 | 0 | (aEvent->mClass == eTouchEventClass ? "touch" : "other")), |
554 | 0 | mozilla::layers::Stringify(aPointRelativeToRootFrame).c_str(), aRootFrame); |
555 | 0 |
|
556 | 0 | const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->mClass); |
557 | 0 | if (!prefs || !prefs->mEnabled) { |
558 | 0 | PET_LOG("Retargeting disabled\n"); |
559 | 0 | return target; |
560 | 0 | } |
561 | 0 | nsIContent* clickableAncestor = nullptr; |
562 | 0 | if (target) { |
563 | 0 | clickableAncestor = GetClickableAncestor(target, nsGkAtoms::body); |
564 | 0 | if (clickableAncestor) { |
565 | 0 | if (!IsElementClickableAndReadable(target, aEvent, prefs)) { |
566 | 0 | aEvent->AsMouseEventBase()->hitCluster = true; |
567 | 0 | } |
568 | 0 | PET_LOG("Target %p is clickable\n", target); |
569 | 0 | // If the target that was directly hit has a clickable ancestor, that |
570 | 0 | // means it too is clickable. And since it is the same as or a descendant |
571 | 0 | // of clickableAncestor, it should become the root for the GetClosest |
572 | 0 | // search. |
573 | 0 | clickableAncestor = target->GetContent(); |
574 | 0 | } |
575 | 0 | } |
576 | 0 |
|
577 | 0 | // Do not modify targeting for actual mouse hardware; only for mouse |
578 | 0 | // events generated by touch-screen hardware. |
579 | 0 | if (aEvent->mClass == eMouseEventClass && |
580 | 0 | prefs->mTouchOnly && |
581 | 0 | aEvent->AsMouseEvent()->inputSource != |
582 | 0 | MouseEvent_Binding::MOZ_SOURCE_TOUCH) { |
583 | 0 | PET_LOG("Mouse input event is not from a touch source\n"); |
584 | 0 | return target; |
585 | 0 | } |
586 | 0 | |
587 | 0 | // If the exact target is non-null, only consider candidate targets in the same |
588 | 0 | // document as the exact target. Otherwise, if an ancestor document has |
589 | 0 | // a mouse event handler for example, targets that are !GetClickableAncestor can |
590 | 0 | // never be targeted --- something nsSubDocumentFrame in an ancestor document |
591 | 0 | // would be targeted instead. |
592 | 0 | nsIFrame* restrictToDescendants = target ? |
593 | 0 | target->PresShell()->GetRootFrame() : aRootFrame; |
594 | 0 |
|
595 | 0 | nsRect targetRect = GetTargetRect(aRootFrame, aPointRelativeToRootFrame, |
596 | 0 | restrictToDescendants, prefs, aFlags); |
597 | 0 | PET_LOG("Expanded point to target rect %s\n", |
598 | 0 | mozilla::layers::Stringify(targetRect).c_str()); |
599 | 0 | AutoTArray<nsIFrame*,8> candidates; |
600 | 0 | nsresult rv = nsLayoutUtils::GetFramesForArea(aRootFrame, targetRect, candidates, flags); |
601 | 0 | if (NS_FAILED(rv)) { |
602 | 0 | return target; |
603 | 0 | } |
604 | 0 | |
605 | 0 | int32_t elementsInCluster = 0; |
606 | 0 |
|
607 | 0 | nsIFrame* closestClickable = |
608 | 0 | GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs, |
609 | 0 | restrictToDescendants, clickableAncestor, candidates, |
610 | 0 | &elementsInCluster); |
611 | 0 | if (closestClickable) { |
612 | 0 | if ((prefs->mTouchClusterDetectionEnabled && elementsInCluster > 1) || |
613 | 0 | (!IsElementClickableAndReadable(closestClickable, aEvent, prefs))) { |
614 | 0 | if (aEvent->mClass == eMouseEventClass) { |
615 | 0 | WidgetMouseEventBase* mouseEventBase = aEvent->AsMouseEventBase(); |
616 | 0 | mouseEventBase->hitCluster = true; |
617 | 0 | } |
618 | 0 | } |
619 | 0 | target = closestClickable; |
620 | 0 | } |
621 | 0 | PET_LOG("Final target is %p\n", target); |
622 | 0 |
|
623 | 0 | // Uncomment this to dump the frame tree to help with debugging. |
624 | 0 | // Note that dumping the frame tree at the top of the function may flood |
625 | 0 | // logcat on Android devices and cause the PET_LOGs to get dropped. |
626 | 0 | // aRootFrame->DumpFrameTree(); |
627 | 0 |
|
628 | 0 | if (!target || !prefs->mRepositionEventCoords) { |
629 | 0 | // No repositioning required for this event |
630 | 0 | return target; |
631 | 0 | } |
632 | 0 | |
633 | 0 | // Take the point relative to the root frame, make it relative to the target, |
634 | 0 | // clamp it to the bounds, and then make it relative to the root frame again. |
635 | 0 | nsPoint point = aPointRelativeToRootFrame; |
636 | 0 | if (nsLayoutUtils::TRANSFORM_SUCCEEDED != nsLayoutUtils::TransformPoint(aRootFrame, target, point)) { |
637 | 0 | return target; |
638 | 0 | } |
639 | 0 | point = target->GetRectRelativeToSelf().ClampPoint(point); |
640 | 0 | if (nsLayoutUtils::TRANSFORM_SUCCEEDED != nsLayoutUtils::TransformPoint(target, aRootFrame, point)) { |
641 | 0 | return target; |
642 | 0 | } |
643 | 0 | // Now we basically undo the operations in GetEventCoordinatesRelativeTo, to |
644 | 0 | // get back the (now-clamped) coordinates in the event's widget's space. |
645 | 0 | nsView* view = aRootFrame->GetView(); |
646 | 0 | if (!view) { |
647 | 0 | return target; |
648 | 0 | } |
649 | 0 | LayoutDeviceIntPoint widgetPoint = nsLayoutUtils::TranslateViewToWidget( |
650 | 0 | aRootFrame->PresContext(), view, point, aEvent->mWidget); |
651 | 0 | if (widgetPoint.x != NS_UNCONSTRAINEDSIZE) { |
652 | 0 | // If that succeeded, we update the point in the event |
653 | 0 | aEvent->mRefPoint = widgetPoint; |
654 | 0 | } |
655 | 0 | return target; |
656 | 0 | } |
657 | | |
658 | | } // namespace mozilla |