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