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