/src/mozilla-central/layout/base/AccessibleCaret.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 "AccessibleCaret.h" |
8 | | |
9 | | #include "AccessibleCaretLogger.h" |
10 | | #include "mozilla/FloatingPoint.h" |
11 | | #include "mozilla/StaticPrefs.h" |
12 | | #include "mozilla/ToString.h" |
13 | | #include "nsCanvasFrame.h" |
14 | | #include "nsCaret.h" |
15 | | #include "nsCSSFrameConstructor.h" |
16 | | #include "nsDOMTokenList.h" |
17 | | #include "nsIFrame.h" |
18 | | #include "nsPlaceholderFrame.h" |
19 | | |
20 | | namespace mozilla { |
21 | | using namespace dom; |
22 | | |
23 | | #undef AC_LOG |
24 | | #define AC_LOG(message, ...) \ |
25 | 0 | AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__); |
26 | | |
27 | | #undef AC_LOGV |
28 | | #define AC_LOGV(message, ...) \ |
29 | | AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__); |
30 | | |
31 | | NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener, nsIDOMEventListener) |
32 | | |
33 | | NS_NAMED_LITERAL_STRING(AccessibleCaret::sTextOverlayElementId, "text-overlay"); |
34 | | NS_NAMED_LITERAL_STRING(AccessibleCaret::sCaretImageElementId, "image"); |
35 | | |
36 | 0 | #define AC_PROCESS_ENUM_TO_STREAM(e) case(e): aStream << #e; break; |
37 | | std::ostream& |
38 | | operator<<(std::ostream& aStream, const AccessibleCaret::Appearance& aAppearance) |
39 | 0 | { |
40 | 0 | using Appearance = AccessibleCaret::Appearance; |
41 | 0 | switch (aAppearance) { |
42 | 0 | AC_PROCESS_ENUM_TO_STREAM(Appearance::None); |
43 | 0 | AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal); |
44 | 0 | AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown); |
45 | 0 | AC_PROCESS_ENUM_TO_STREAM(Appearance::Left); |
46 | 0 | AC_PROCESS_ENUM_TO_STREAM(Appearance::Right); |
47 | 0 | } |
48 | 0 | return aStream; |
49 | 0 | } |
50 | | |
51 | | std::ostream& |
52 | | operator<<(std::ostream& aStream, |
53 | | const AccessibleCaret::PositionChangedResult& aResult) |
54 | 0 | { |
55 | 0 | using PositionChangedResult = AccessibleCaret::PositionChangedResult; |
56 | 0 | switch (aResult) { |
57 | 0 | AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged); |
58 | 0 | AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Changed); |
59 | 0 | AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible); |
60 | 0 | } |
61 | 0 | return aStream; |
62 | 0 | } |
63 | | #undef AC_PROCESS_ENUM_TO_STREAM |
64 | | |
65 | | // ----------------------------------------------------------------------------- |
66 | | // Implementation of AccessibleCaret methods |
67 | | |
68 | | AccessibleCaret::AccessibleCaret(nsIPresShell* aPresShell) |
69 | | : mPresShell(aPresShell) |
70 | 0 | { |
71 | 0 | // Check all resources required. |
72 | 0 | if (mPresShell) { |
73 | 0 | MOZ_ASSERT(RootFrame()); |
74 | 0 | MOZ_ASSERT(mPresShell->GetDocument()); |
75 | 0 | InjectCaretElement(mPresShell->GetDocument()); |
76 | 0 | } |
77 | 0 | } |
78 | | |
79 | | AccessibleCaret::~AccessibleCaret() |
80 | 0 | { |
81 | 0 | if (mPresShell) { |
82 | 0 | RemoveCaretElement(mPresShell->GetDocument()); |
83 | 0 | } |
84 | 0 | } |
85 | | |
86 | | void |
87 | | AccessibleCaret::SetAppearance(Appearance aAppearance) |
88 | 0 | { |
89 | 0 | if (mAppearance == aAppearance) { |
90 | 0 | return; |
91 | 0 | } |
92 | 0 | |
93 | 0 | ErrorResult rv; |
94 | 0 | CaretElement().ClassList()->Remove(AppearanceString(mAppearance), rv); |
95 | 0 | MOZ_ASSERT(!rv.Failed(), "Remove old appearance failed!"); |
96 | 0 |
|
97 | 0 | CaretElement().ClassList()->Add(AppearanceString(aAppearance), rv); |
98 | 0 | MOZ_ASSERT(!rv.Failed(), "Add new appearance failed!"); |
99 | 0 |
|
100 | 0 | AC_LOG("%s: %s -> %s", __FUNCTION__, ToString(mAppearance).c_str(), |
101 | 0 | ToString(aAppearance).c_str()); |
102 | 0 |
|
103 | 0 | mAppearance = aAppearance; |
104 | 0 |
|
105 | 0 | // Need to reset rect since the cached rect will be compared in SetPosition. |
106 | 0 | if (mAppearance == Appearance::None) { |
107 | 0 | mImaginaryCaretRect = nsRect(); |
108 | 0 | mZoomLevel = 0.0f; |
109 | 0 | } |
110 | 0 | } |
111 | | |
112 | | /* static */ nsAutoString |
113 | | AccessibleCaret::AppearanceString(Appearance aAppearance) |
114 | 0 | { |
115 | 0 | nsAutoString string; |
116 | 0 | switch (aAppearance) { |
117 | 0 | case Appearance::None: |
118 | 0 | case Appearance::NormalNotShown: |
119 | 0 | string = NS_LITERAL_STRING("none"); |
120 | 0 | break; |
121 | 0 | case Appearance::Normal: |
122 | 0 | string = NS_LITERAL_STRING("normal"); |
123 | 0 | break; |
124 | 0 | case Appearance::Right: |
125 | 0 | string = NS_LITERAL_STRING("right"); |
126 | 0 | break; |
127 | 0 | case Appearance::Left: |
128 | 0 | string = NS_LITERAL_STRING("left"); |
129 | 0 | break; |
130 | 0 | } |
131 | 0 | return string; |
132 | 0 | } |
133 | | |
134 | | bool |
135 | | AccessibleCaret::Intersects(const AccessibleCaret& aCaret) const |
136 | 0 | { |
137 | 0 | MOZ_ASSERT(mPresShell == aCaret.mPresShell); |
138 | 0 |
|
139 | 0 | if (!IsVisuallyVisible() || !aCaret.IsVisuallyVisible()) { |
140 | 0 | return false; |
141 | 0 | } |
142 | 0 | |
143 | 0 | nsRect rect = nsLayoutUtils::GetRectRelativeToFrame(&CaretElement(), RootFrame()); |
144 | 0 | nsRect rhsRect = nsLayoutUtils::GetRectRelativeToFrame(&aCaret.CaretElement(), RootFrame()); |
145 | 0 | return rect.Intersects(rhsRect); |
146 | 0 | } |
147 | | |
148 | | bool |
149 | | AccessibleCaret::Contains(const nsPoint& aPoint, TouchArea aTouchArea) const |
150 | 0 | { |
151 | 0 | if (!IsVisuallyVisible()) { |
152 | 0 | return false; |
153 | 0 | } |
154 | 0 | |
155 | 0 | nsRect textOverlayRect = |
156 | 0 | nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame()); |
157 | 0 | nsRect caretImageRect = |
158 | 0 | nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame()); |
159 | 0 |
|
160 | 0 | if (aTouchArea == TouchArea::CaretImage) { |
161 | 0 | return caretImageRect.Contains(aPoint); |
162 | 0 | } |
163 | 0 | |
164 | 0 | MOZ_ASSERT(aTouchArea == TouchArea::Full, "Unexpected TouchArea type!"); |
165 | 0 | return textOverlayRect.Contains(aPoint) || caretImageRect.Contains(aPoint); |
166 | 0 | } |
167 | | |
168 | | void |
169 | | AccessibleCaret::EnsureApzAware() |
170 | 0 | { |
171 | 0 | // If the caret element was cloned, the listener might have been lost. So |
172 | 0 | // if that's the case we register a dummy listener if there isn't one on |
173 | 0 | // the element already. |
174 | 0 | if (!CaretElement().IsApzAware()) { |
175 | 0 | // FIXME(emilio): Is this needed anymore? |
176 | 0 | CaretElement().AddEventListener(NS_LITERAL_STRING("touchstart"), |
177 | 0 | mDummyTouchListener, false); |
178 | 0 | } |
179 | 0 | } |
180 | | |
181 | | void |
182 | | AccessibleCaret::InjectCaretElement(nsIDocument* aDocument) |
183 | 0 | { |
184 | 0 | ErrorResult rv; |
185 | 0 | RefPtr<Element> element = CreateCaretElement(aDocument); |
186 | 0 | mCaretElementHolder = aDocument->InsertAnonymousContent(*element, rv); |
187 | 0 |
|
188 | 0 | MOZ_ASSERT(!rv.Failed(), "Insert anonymous content should not fail!"); |
189 | 0 | MOZ_ASSERT(mCaretElementHolder, "We must have anonymous content!"); |
190 | 0 |
|
191 | 0 | // InsertAnonymousContent will clone the element to make an AnonymousContent. |
192 | 0 | // Since event listeners are not being cloned when cloning a node, we need to |
193 | 0 | // add the listener here. |
194 | 0 | EnsureApzAware(); |
195 | 0 | } |
196 | | |
197 | | already_AddRefed<Element> |
198 | | AccessibleCaret::CreateCaretElement(nsIDocument* aDocument) const |
199 | 0 | { |
200 | 0 | // Content structure of AccessibleCaret |
201 | 0 | // <div class="moz-accessiblecaret"> <- CaretElement() |
202 | 0 | // <div id="text-overlay" <- TextOverlayElement() |
203 | 0 | // <div id="image"> <- CaretImageElement() |
204 | 0 |
|
205 | 0 | ErrorResult rv; |
206 | 0 | RefPtr<Element> parent = aDocument->CreateHTMLElement(nsGkAtoms::div); |
207 | 0 | parent->ClassList()->Add(NS_LITERAL_STRING("moz-accessiblecaret"), rv); |
208 | 0 | parent->ClassList()->Add(NS_LITERAL_STRING("none"), rv); |
209 | 0 |
|
210 | 0 | auto CreateAndAppendChildElement = [aDocument, &parent]( |
211 | 0 | const nsLiteralString& aElementId) |
212 | 0 | { |
213 | 0 | RefPtr<Element> child = aDocument->CreateHTMLElement(nsGkAtoms::div); |
214 | 0 | child->SetAttr(kNameSpaceID_None, nsGkAtoms::id, aElementId, true); |
215 | 0 | parent->AppendChildTo(child, false); |
216 | 0 | }; |
217 | 0 |
|
218 | 0 | CreateAndAppendChildElement(sTextOverlayElementId); |
219 | 0 | CreateAndAppendChildElement(sCaretImageElementId); |
220 | 0 |
|
221 | 0 | return parent.forget(); |
222 | 0 | } |
223 | | |
224 | | void |
225 | | AccessibleCaret::RemoveCaretElement(nsIDocument* aDocument) |
226 | 0 | { |
227 | 0 | CaretElement().RemoveEventListener(NS_LITERAL_STRING("touchstart"), |
228 | 0 | mDummyTouchListener, false); |
229 | 0 |
|
230 | 0 | // FIXME(emilio): This shouldn't be needed and should be done by |
231 | 0 | // ContentRemoved via RemoveAnonymousContent, but the current setup tears down |
232 | 0 | // the accessible caret manager after the shell has stopped observing the |
233 | 0 | // document, but before the frame tree has gone away. This could clearly be |
234 | 0 | // better... |
235 | 0 | if (nsIFrame* frame = CaretElement().GetPrimaryFrame()) { |
236 | 0 | if (frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) { |
237 | 0 | frame = frame->GetPlaceholderFrame(); |
238 | 0 | } |
239 | 0 | nsAutoScriptBlocker scriptBlocker; |
240 | 0 | frame->GetParent()->RemoveFrame(nsIFrame::kPrincipalList, frame); |
241 | 0 | } |
242 | 0 |
|
243 | 0 | aDocument->RemoveAnonymousContent(*mCaretElementHolder, IgnoreErrors()); |
244 | 0 | } |
245 | | |
246 | | AccessibleCaret::PositionChangedResult |
247 | | AccessibleCaret::SetPosition(nsIFrame* aFrame, int32_t aOffset) |
248 | 0 | { |
249 | 0 | if (!CustomContentContainerFrame()) { |
250 | 0 | return PositionChangedResult::NotChanged; |
251 | 0 | } |
252 | 0 | |
253 | 0 | nsRect imaginaryCaretRectInFrame = |
254 | 0 | nsCaret::GetGeometryForFrame(aFrame, aOffset, nullptr); |
255 | 0 |
|
256 | 0 | imaginaryCaretRectInFrame = |
257 | 0 | nsLayoutUtils::ClampRectToScrollFrames(aFrame, imaginaryCaretRectInFrame); |
258 | 0 |
|
259 | 0 | if (imaginaryCaretRectInFrame.IsEmpty()) { |
260 | 0 | // Don't bother to set the caret position since it's invisible. |
261 | 0 | mImaginaryCaretRect = nsRect(); |
262 | 0 | mZoomLevel = 0.0f; |
263 | 0 | return PositionChangedResult::Invisible; |
264 | 0 | } |
265 | 0 | |
266 | 0 | nsRect imaginaryCaretRect = imaginaryCaretRectInFrame; |
267 | 0 | nsLayoutUtils::TransformRect(aFrame, RootFrame(), imaginaryCaretRect); |
268 | 0 | float zoomLevel = GetZoomLevel(); |
269 | 0 |
|
270 | 0 | if (imaginaryCaretRect.IsEqualEdges(mImaginaryCaretRect) && |
271 | 0 | FuzzyEqualsMultiplicative(zoomLevel, mZoomLevel)) { |
272 | 0 | return PositionChangedResult::NotChanged; |
273 | 0 | } |
274 | 0 | |
275 | 0 | mImaginaryCaretRect = imaginaryCaretRect; |
276 | 0 | mZoomLevel = zoomLevel; |
277 | 0 |
|
278 | 0 | // SetCaretElementStyle() requires the input rect relative to container frame. |
279 | 0 | nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame; |
280 | 0 | nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(), |
281 | 0 | imaginaryCaretRectInContainerFrame); |
282 | 0 | SetCaretElementStyle(imaginaryCaretRectInContainerFrame, mZoomLevel); |
283 | 0 |
|
284 | 0 | return PositionChangedResult::Changed; |
285 | 0 | } |
286 | | |
287 | | nsIFrame* |
288 | | AccessibleCaret::CustomContentContainerFrame() const |
289 | 0 | { |
290 | 0 | nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame(); |
291 | 0 | Element* container = canvasFrame->GetCustomContentContainer(); |
292 | 0 | nsIFrame* containerFrame = container->GetPrimaryFrame(); |
293 | 0 | return containerFrame; |
294 | 0 | } |
295 | | |
296 | | void |
297 | | AccessibleCaret::SetCaretElementStyle(const nsRect& aRect, float aZoomLevel) |
298 | 0 | { |
299 | 0 | nsPoint position = CaretElementPosition(aRect); |
300 | 0 | nsAutoString styleStr; |
301 | 0 | styleStr.AppendPrintf("left: %dpx; top: %dpx; " |
302 | 0 | "width: ", |
303 | 0 | nsPresContext::AppUnitsToIntCSSPixels(position.x), |
304 | 0 | nsPresContext::AppUnitsToIntCSSPixels(position.y)); |
305 | 0 | // We can't use AppendPrintf here, because it does locale-specific |
306 | 0 | // formatting of floating-point values. |
307 | 0 | styleStr.AppendFloat(StaticPrefs::layout_accessiblecaret_width() / aZoomLevel); |
308 | 0 | styleStr.AppendLiteral("px; height: "); |
309 | 0 | styleStr.AppendFloat(StaticPrefs::layout_accessiblecaret_height() / aZoomLevel); |
310 | 0 | styleStr.AppendLiteral("px; margin-left: "); |
311 | 0 | styleStr.AppendFloat( |
312 | 0 | StaticPrefs::layout_accessiblecaret_margin_left() / aZoomLevel); |
313 | 0 | styleStr.AppendLiteral("px"); |
314 | 0 |
|
315 | 0 | CaretElement().SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true); |
316 | 0 | AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get()); |
317 | 0 |
|
318 | 0 | // Set style string for children. |
319 | 0 | SetTextOverlayElementStyle(aRect, aZoomLevel); |
320 | 0 | SetCaretImageElementStyle(aRect, aZoomLevel); |
321 | 0 | } |
322 | | |
323 | | void |
324 | | AccessibleCaret::SetTextOverlayElementStyle(const nsRect& aRect, |
325 | | float aZoomLevel) |
326 | 0 | { |
327 | 0 | nsAutoString styleStr; |
328 | 0 | styleStr.AppendPrintf("height: %dpx;", |
329 | 0 | nsPresContext::AppUnitsToIntCSSPixels(aRect.height)); |
330 | 0 | TextOverlayElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, |
331 | 0 | true); |
332 | 0 | AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get()); |
333 | 0 | } |
334 | | |
335 | | void |
336 | | AccessibleCaret::SetCaretImageElementStyle(const nsRect& aRect, |
337 | | float aZoomLevel) |
338 | 0 | { |
339 | 0 | nsAutoString styleStr; |
340 | 0 | styleStr.AppendPrintf("margin-top: %dpx;", |
341 | 0 | nsPresContext::AppUnitsToIntCSSPixels(aRect.height)); |
342 | 0 | CaretImageElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, |
343 | 0 | true); |
344 | 0 | AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get()); |
345 | 0 | } |
346 | | |
347 | | float |
348 | | AccessibleCaret::GetZoomLevel() |
349 | 0 | { |
350 | 0 | // Full zoom on desktop. |
351 | 0 | float fullZoom = mPresShell->GetPresContext()->GetFullZoom(); |
352 | 0 |
|
353 | 0 | // Pinch-zoom on fennec. |
354 | 0 | float resolution = mPresShell->GetCumulativeResolution(); |
355 | 0 |
|
356 | 0 | return fullZoom * resolution; |
357 | 0 | } |
358 | | |
359 | | } // namespace mozilla |