Bug 1110039 - Part 2.2 - Add AccessibleCaret. r=roc

See AccessibleCaret.h for the class description.

Technical difference between AccessibleCaret and Touch/SelectionCarets:
The anonymous dom element containing a caret image will be created by
AccessibleCaret by using the API landed in bug 1020244 instead of being
created by nsCanvasFrame.
This commit is contained in:
Ting-Yu Lin 2015-05-04 21:25:00 +02:00
parent 7116f8917d
commit 206548e5fb
3 changed files with 572 additions and 0 deletions

View File

@ -0,0 +1,269 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "AccessibleCaret.h"
#include "AccessibleCaretLogger.h"
#include "mozilla/Preferences.h"
#include "nsCanvasFrame.h"
#include "nsCaret.h"
#include "nsDOMTokenList.h"
#include "nsIFrame.h"
namespace mozilla {
using namespace dom;
#undef AC_LOG
#define AC_LOG(message, ...) \
AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
#undef AC_LOGV
#define AC_LOGV(message, ...) \
AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener, nsIDOMEventListener)
// -----------------------------------------------------------------------------
// Implementation of AccessibleCaret methods
AccessibleCaret::AccessibleCaret(nsIPresShell* aPresShell)
: mPresShell(aPresShell)
{
// Check all resources required.
MOZ_ASSERT(mPresShell);
MOZ_ASSERT(RootFrame());
MOZ_ASSERT(mPresShell->GetDocument());
MOZ_ASSERT(mPresShell->GetCanvasFrame());
MOZ_ASSERT(mPresShell->GetCanvasFrame()->GetCustomContentContainer());
InjectCaretElement(mPresShell->GetDocument());
}
AccessibleCaret::~AccessibleCaret()
{
RemoveCaretElement(mPresShell->GetDocument());
}
void
AccessibleCaret::SetAppearance(Appearance aAppearance)
{
if (mAppearance == aAppearance) {
return;
}
ErrorResult rv;
CaretElement()->ClassList()->Remove(AppearanceString(mAppearance), rv);
MOZ_ASSERT(!rv.Failed(), "Remove old appearance failed!");
CaretElement()->ClassList()->Add(AppearanceString(aAppearance), rv);
MOZ_ASSERT(!rv.Failed(), "Add new appearance failed!");
mAppearance = aAppearance;
// Need to reset rect since the cached rect will be compared in SetPosition.
if (mAppearance == Appearance::None) {
mImaginaryCaretRect = nsRect();
}
}
void
AccessibleCaret::SetSelectionBarEnabled(bool aEnabled)
{
if (mSelectionBarEnabled == aEnabled) {
return;
}
AC_LOG("%s, enabled %d", __FUNCTION__, aEnabled);
ErrorResult rv;
CaretElement()->ClassList()->Toggle(NS_LITERAL_STRING("no-bar"),
Optional<bool>(!aEnabled), rv);
MOZ_ASSERT(!rv.Failed());
mSelectionBarEnabled = aEnabled;
}
/* static */ nsString
AccessibleCaret::AppearanceString(Appearance aAppearance)
{
nsAutoString string;
switch (aAppearance) {
case Appearance::None:
case Appearance::NormalNotShown:
string = NS_LITERAL_STRING("none");
break;
case Appearance::Normal:
string = NS_LITERAL_STRING("normal");
break;
case Appearance::Right:
string = NS_LITERAL_STRING("right");
break;
case Appearance::Left:
string = NS_LITERAL_STRING("left");
break;
}
return string;
}
bool
AccessibleCaret::Intersects(const AccessibleCaret& aCaret) const
{
MOZ_ASSERT(mPresShell == aCaret.mPresShell);
if (!IsVisuallyVisible() || !aCaret.IsVisuallyVisible()) {
return false;
}
nsRect rect = nsLayoutUtils::GetRectRelativeToFrame(CaretElement(), RootFrame());
nsRect rhsRect = nsLayoutUtils::GetRectRelativeToFrame(aCaret.CaretElement(), RootFrame());
return rect.Intersects(rhsRect);
}
bool
AccessibleCaret::Contains(const nsPoint& aPoint) const
{
if (!IsVisuallyVisible()) {
return false;
}
nsRect rect =
nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
return rect.Contains(aPoint);
}
void
AccessibleCaret::InjectCaretElement(nsIDocument* aDocument)
{
ErrorResult rv;
nsCOMPtr<Element> element = CreateCaretElement(aDocument);
mCaretElementHolder = aDocument->InsertAnonymousContent(*element, rv);
MOZ_ASSERT(!rv.Failed(), "Insert anonymous content should not fail!");
MOZ_ASSERT(mCaretElementHolder.get(), "We must have anonymous content!");
// InsertAnonymousContent will clone the element to make an AnonymousContent.
// Since event listeners are not being cloned when cloning a node, we need to
// add the listener here.
CaretElement()->AddEventListener(NS_LITERAL_STRING("touchstart"),
mDummyTouchListener, false);
}
already_AddRefed<Element>
AccessibleCaret::CreateCaretElement(nsIDocument* aDocument) const
{
// Content structure of AccessibleCaret
// <div class="moz-accessiblecaret"> <- CaretElement()
// <div class="image"> <- CaretImageElement()
// <div class="bar"> <- SelectionBarElement()
ErrorResult rv;
nsCOMPtr<Element> parent = aDocument->CreateHTMLElement(nsGkAtoms::div);
parent->ClassList()->Add(NS_LITERAL_STRING("moz-accessiblecaret"), rv);
parent->ClassList()->Add(NS_LITERAL_STRING("none"), rv);
parent->ClassList()->Add(NS_LITERAL_STRING("no-bar"), rv);
nsCOMPtr<Element> image = aDocument->CreateHTMLElement(nsGkAtoms::div);
image->ClassList()->Add(NS_LITERAL_STRING("image"), rv);
parent->AppendChildTo(image, false);
nsCOMPtr<Element> bar = aDocument->CreateHTMLElement(nsGkAtoms::div);
bar->ClassList()->Add(NS_LITERAL_STRING("bar"), rv);
parent->AppendChildTo(bar, false);
return parent.forget();
}
void
AccessibleCaret::RemoveCaretElement(nsIDocument* aDocument)
{
CaretElement()->RemoveEventListener(NS_LITERAL_STRING("touchstart"),
mDummyTouchListener, false);
ErrorResult rv;
aDocument->RemoveAnonymousContent(*mCaretElementHolder, rv);
// It's OK rv is failed since nsCanvasFrame might not exists now.
}
AccessibleCaret::PositionChangedResult
AccessibleCaret::SetPosition(nsIFrame* aFrame, int32_t aOffset)
{
if (!CustomContentContainerFrame()) {
return PositionChangedResult::NotChanged;
}
nsRect imaginaryCaretRectInFrame =
nsCaret::GetGeometryForFrame(aFrame, aOffset, nullptr);
imaginaryCaretRectInFrame =
nsLayoutUtils::ClampRectToScrollFrames(aFrame, imaginaryCaretRectInFrame);
if (imaginaryCaretRectInFrame.IsEmpty()) {
// Don't bother to set the caret position since it's invisible.
return PositionChangedResult::Invisible;
}
nsRect imaginaryCaretRect = imaginaryCaretRectInFrame;
nsLayoutUtils::TransformRect(aFrame, RootFrame(), imaginaryCaretRect);
if (imaginaryCaretRect.IsEqualEdges(mImaginaryCaretRect)) {
return PositionChangedResult::NotChanged;
}
mImaginaryCaretRect = imaginaryCaretRect;
// SetCaretElementPosition() and SetSelectionBarElementPosition() require the
// input rect relative to container frame.
nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame;
nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(),
imaginaryCaretRectInContainerFrame);
SetCaretElementPosition(imaginaryCaretRectInContainerFrame);
SetSelectionBarElementPosition(imaginaryCaretRectInContainerFrame);
return PositionChangedResult::Changed;
}
nsIFrame*
AccessibleCaret::CustomContentContainerFrame() const
{
nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame();
Element* container = canvasFrame->GetCustomContentContainer();
nsIFrame* containerFrame = container->GetPrimaryFrame();
return containerFrame;
}
void
AccessibleCaret::SetCaretElementPosition(const nsRect& aRect)
{
nsPoint position = CaretElementPosition(aRect);
nsAutoString styleStr;
styleStr.AppendPrintf("left: %dpx; top: %dpx;",
nsPresContext::AppUnitsToIntCSSPixels(position.x),
nsPresContext::AppUnitsToIntCSSPixels(position.y));
ErrorResult rv;
CaretElement()->SetAttribute(NS_LITERAL_STRING("style"), styleStr, rv);
MOZ_ASSERT(!rv.Failed());
AC_LOG("Set caret style: %s", NS_ConvertUTF16toUTF8(styleStr).get());
}
void
AccessibleCaret::SetSelectionBarElementPosition(const nsRect& aRect)
{
int32_t height = nsPresContext::AppUnitsToIntCSSPixels(aRect.height);
nsAutoString barStyleStr;
barStyleStr.AppendPrintf("margin-top: -%dpx; height: %dpx;",
height, height);
ErrorResult rv;
SelectionBarElement()->SetAttribute(NS_LITERAL_STRING("style"), barStyleStr, rv);
MOZ_ASSERT(!rv.Failed());
AC_LOG("Set bar style: %s", NS_ConvertUTF16toUTF8(barStyleStr).get());
}
} // namespace mozilla

View File

@ -0,0 +1,209 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef AccessibleCaret_h__
#define AccessibleCaret_h__
#include "mozilla/Attributes.h"
#include "mozilla/dom/AnonymousContent.h"
#include "mozilla/dom/Element.h"
#include "nsCOMPtr.h"
#include "nsIDOMEventListener.h"
#include "nsISupportsBase.h"
#include "nsISupportsImpl.h"
#include "nsRect.h"
#include "nsRefPtr.h"
#include "nsString.h"
class nsIDocument;
class nsIFrame;
class nsIPresShell;
struct nsPoint;
namespace mozilla {
// -----------------------------------------------------------------------------
// Upon the creation of AccessibleCaret, it will insert DOM Element as an
// anonymous content containing the caret image. The caret appearance and
// position can be controlled by SetAppearance() and SetPosition().
//
// All the rect or point are relative to root frame except being specified
// explicitly.
//
// None of the methods in AccessibleCaret will flush layout or style. To ensure
// that SetPosition() works correctly, the caller must make sure the layout is
// up to date.
//
class AccessibleCaret final
{
public:
explicit AccessibleCaret(nsIPresShell* aPresShell);
~AccessibleCaret();
// This enumeration representing the visibility and visual style of an
// AccessibleCaret.
//
// Use SetAppearance() to change the appearance, and use GetAppearance() to
// get the current appearance.
enum class Appearance : uint8_t {
// Do not display the caret at all.
None,
// Display the caret in default style.
Normal,
// The caret should be displayed logically but it is kept invisible to the
// user. This enum is the only difference between "logically visible" and
// "visually visible". It can be used for reasons such as:
// 1. Out of scroll port.
// 2. For UX requirement such as hide a caret in an empty text area.
NormalNotShown,
// Display the caret which is tilted to the left.
Left,
// Display the caret which is tilted to the right.
Right
};
Appearance GetAppearance() const
{
return mAppearance;
}
void SetAppearance(Appearance aAppearance);
// Return true if current appearance is either Normal, NormalNotShown, Left,
// or Right.
bool IsLogicallyVisible() const
{
return mAppearance != Appearance::None;
}
// Return true if current appearance is either Normal, Left, or Right.
bool IsVisuallyVisible() const
{
return (mAppearance != Appearance::None) &&
(mAppearance != Appearance::NormalNotShown);
}
// Set true to enable the "Text Selection Bar" described in "Text Selection
// Visual Spec" in bug 921965.
void SetSelectionBarEnabled(bool aEnabled);
// This enumeration representing the result returned by SetPosition().
enum class PositionChangedResult : uint8_t {
// Position is not changed.
NotChanged,
// Position is changed.
Changed,
// Position is out of scroll port.
Invisible
};
PositionChangedResult SetPosition(nsIFrame* aFrame, int32_t aOffset);
// Does two AccessibleCarets overlap?
bool Intersects(const AccessibleCaret& aCaret) const;
// Is the point within the caret's rect? The point should be relative to root
// frame.
bool Contains(const nsPoint& aPoint) const;
// The geometry center of the imaginary caret (nsCaret) to which this
// AccessibleCaret is attached. It is needed when dragging the caret.
nsPoint LogicalPosition() const
{
return mImaginaryCaretRect.Center();
}
// Element for 'Intersects' test. Container of image and bar elements.
dom::Element* CaretElement() const
{
return mCaretElementHolder->GetContentNode();
}
private:
// Argument aRect should be relative to CustomContentContainerFrame().
void SetCaretElementPosition(const nsRect& aRect);
void SetSelectionBarElementPosition(const nsRect& aRect);
// Element which contains the caret image for 'Contains' test.
dom::Element* CaretImageElement() const
{
return CaretElement()->GetFirstElementChild();
}
// Element which represents the text selection bar.
dom::Element* SelectionBarElement() const
{
return CaretElement()->GetLastElementChild();
}
nsIFrame* RootFrame() const
{
return mPresShell->GetRootFrame();
}
nsIFrame* CustomContentContainerFrame() const;
// Transform Appearance to CSS class name in ua.css.
static nsString AppearanceString(Appearance aAppearance);
already_AddRefed<dom::Element> CreateCaretElement(nsIDocument* aDocument) const;
// Inject caret element into custom content container.
void InjectCaretElement(nsIDocument* aDocument);
// Remove caret element from custom content container.
void RemoveCaretElement(nsIDocument* aDocument);
// The bottom-center of the imaginary caret to which this AccessibleCaret is
// attached.
static nsPoint CaretElementPosition(const nsRect& aRect)
{
return aRect.TopLeft() + nsPoint(aRect.width / 2, aRect.height);
}
class DummyTouchListener final : public nsIDOMEventListener
{
public:
NS_DECL_ISUPPORTS
NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) override
{
return NS_OK;
}
private:
virtual ~DummyTouchListener() {};
};
// Member variables
Appearance mAppearance = Appearance::None;
bool mSelectionBarEnabled = false;
// AccessibleCaretManager owns us. When it's destroyed by
// AccessibleCaretEventHub::Terminate() which is called in
// PresShell::Destroy(), it frees us automatically. No need to worry we
// outlive mPresShell.
nsIPresShell* MOZ_NON_OWNING_REF const mPresShell = nullptr;
nsRefPtr<dom::AnonymousContent> mCaretElementHolder;
// mImaginaryCaretRect is relative to root frame.
nsRect mImaginaryCaretRect;
// A no-op touch-start listener which prevents APZ from panning when dragging
// the caret.
nsRefPtr<DummyTouchListener> mDummyTouchListener{new DummyTouchListener()};
}; // class AccessibleCaret
} // namespace mozilla
#endif // AccessibleCaret_h__

View File

@ -314,6 +314,100 @@ parsererror|sourcetext {
font-size: 12pt;
}
div:-moz-native-anonymous.moz-accessiblecaret,
div:-moz-native-anonymous.moz-accessiblecaret > div.image,
div:-moz-native-anonymous.moz-accessiblecaret > div.bar {
position: absolute;
z-index: 2147483647;
}
div:-moz-native-anonymous.moz-accessiblecaret {
width: 44px;
height: 47px;
margin-left: -23px;
}
div:-moz-native-anonymous.moz-accessiblecaret > div.image {
background-position: center center;
background-size: 100% 100%;
width: 100%;
height: 100%;
/* Override this property in moz-custom-content-container to make dummy touch
* listener work. */
pointer-events: auto;
}
div:-moz-native-anonymous.moz-accessiblecaret > div.bar {
margin-left: 49%;
width: 2px;
background-color: #008aa0;
}
div:-moz-native-anonymous.moz-accessiblecaret.no-bar > div.bar {
display: none;
}
div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
background-image: url("resource://gre/res/text_caret.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
background-image: url("resource://gre/res/text_caret_tilt_left.png");
margin-left: -39%;
}
div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
background-image: url("resource://gre/res/text_caret_tilt_right.png");
margin-left: 41%;
}
div:-moz-native-anonymous.moz-accessiblecaret.none {
display: none;
}
@media (min-resolution: 1.5dppx) {
div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
background-image: url("resource://gre/res/text_caret@1.5x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
background-image: url("resource://gre/res/text_caret_tilt_left@1.5x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
background-image: url("resource://gre/res/text_caret_tilt_right@1.5x.png");
}
}
@media (min-resolution: 2dppx) {
div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
background-image: url("resource://gre/res/text_caret@2x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
background-image: url("resource://gre/res/text_caret_tilt_left@2x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
background-image: url("resource://gre/res/text_caret_tilt_right@2x.png");
}
}
@media (min-resolution: 2.25dppx) {
div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
background-image: url("resource://gre/res/text_caret@2.25x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
background-image: url("resource://gre/res/text_caret_tilt_left@2.25x.png");
}
div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
background-image: url("resource://gre/res/text_caret_tilt_right@2.25x.png");
}
}
div:-moz-native-anonymous.moz-touchcaret,
div:-moz-native-anonymous.moz-selectioncaret-left,
div:-moz-native-anonymous.moz-selectioncaret-right {