fune/layout/xul/nsTextBoxFrame.cpp
Rob Wu 5fec51b0d0 Bug 1484914 - Never append duplicate access key hints r=bzbarsky
On macOS, access keys are not visually observable.

On Linux and Windows, access keys are underlined.
Moreover, if intl.menuitems.alwaysappendaccesskeys is set, the access
key is always appended as "(X)", optionally preceded by a space, where X
is the uppercased version of the access key.

This commit updates the logic to not append a new "(X)" if this expected
suffix is already present at the end of the label.

Differential Revision: https://phabricator.services.mozilla.com/D3996

--HG--
extra : moz-landing-system : lando
2018-08-29 22:00:32 +00:00

1246 lines
40 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=4 sw=4 sts=4 et cindent: */
/* 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 "nsTextBoxFrame.h"
#include "gfx2DGlue.h"
#include "gfxUtils.h"
#include "mozilla/gfx/2D.h"
#include "nsFontMetrics.h"
#include "nsReadableUtils.h"
#include "nsCOMPtr.h"
#include "nsGkAtoms.h"
#include "nsPresContext.h"
#include "gfxContext.h"
#include "mozilla/ComputedStyle.h"
#include "nsIContent.h"
#include "nsNameSpaceManager.h"
#include "nsBoxLayoutState.h"
#include "nsMenuBarListener.h"
#include "nsString.h"
#include "nsIServiceManager.h"
#include "nsIDOMXULLabelElement.h"
#include "mozilla/EventStateManager.h"
#include "nsITheme.h"
#include "nsUnicharUtils.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsCSSRendering.h"
#include "nsIReflowCallback.h"
#include "nsBoxFrame.h"
#include "mozilla/Preferences.h"
#include "nsLayoutUtils.h"
#include "mozilla/Attributes.h"
#include "nsUnicodeProperties.h"
#ifdef ACCESSIBILITY
#include "nsAccessibilityService.h"
#endif
#include "nsBidiUtils.h"
#include "nsBidiPresUtils.h"
using namespace mozilla;
using namespace mozilla::gfx;
class nsAccessKeyInfo
{
public:
int32_t mAccesskeyIndex;
nscoord mBeforeWidth, mAccessWidth, mAccessUnderlineSize, mAccessOffset;
};
bool nsTextBoxFrame::gAlwaysAppendAccessKey = false;
bool nsTextBoxFrame::gAccessKeyPrefInitialized = false;
bool nsTextBoxFrame::gInsertSeparatorBeforeAccessKey = false;
bool nsTextBoxFrame::gInsertSeparatorPrefInitialized = false;
nsIFrame*
NS_NewTextBoxFrame (nsIPresShell* aPresShell, ComputedStyle* aStyle)
{
return new (aPresShell) nsTextBoxFrame(aStyle);
}
NS_IMPL_FRAMEARENA_HELPERS(nsTextBoxFrame)
NS_QUERYFRAME_HEAD(nsTextBoxFrame)
NS_QUERYFRAME_ENTRY(nsTextBoxFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame)
nsresult
nsTextBoxFrame::AttributeChanged(int32_t aNameSpaceID,
nsAtom* aAttribute,
int32_t aModType)
{
bool aResize;
bool aRedraw;
UpdateAttributes(aAttribute, aResize, aRedraw);
if (aResize) {
PresShell()->
FrameNeedsReflow(this, nsIPresShell::eStyleChange,
NS_FRAME_IS_DIRTY);
} else if (aRedraw) {
nsBoxLayoutState state(PresContext());
XULRedraw(state);
}
// If the accesskey changed, register for the new value
// The old value has been unregistered in nsXULElement::SetAttr
if (aAttribute == nsGkAtoms::accesskey || aAttribute == nsGkAtoms::control)
RegUnregAccessKey(true);
return NS_OK;
}
nsTextBoxFrame::nsTextBoxFrame(ComputedStyle* aStyle)
: nsLeafBoxFrame(aStyle, kClassID)
, mAccessKeyInfo(nullptr)
, mCropType(CropRight)
, mAscent(0)
, mNeedsReflowCallback(false)
{
MarkIntrinsicISizesDirty();
}
nsTextBoxFrame::~nsTextBoxFrame()
{
delete mAccessKeyInfo;
}
void
nsTextBoxFrame::Init(nsIContent* aContent,
nsContainerFrame* aParent,
nsIFrame* aPrevInFlow)
{
nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow);
bool aResize;
bool aRedraw;
UpdateAttributes(nullptr, aResize, aRedraw); /* update all */
// register access key
RegUnregAccessKey(true);
}
void
nsTextBoxFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData)
{
// unregister access key
RegUnregAccessKey(false);
nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
}
bool
nsTextBoxFrame::AlwaysAppendAccessKey()
{
if (!gAccessKeyPrefInitialized)
{
gAccessKeyPrefInitialized = true;
const char* prefName = "intl.menuitems.alwaysappendaccesskeys";
nsAutoString val;
Preferences::GetLocalizedString(prefName, val);
gAlwaysAppendAccessKey = val.EqualsLiteral("true");
}
return gAlwaysAppendAccessKey;
}
bool
nsTextBoxFrame::InsertSeparatorBeforeAccessKey()
{
if (!gInsertSeparatorPrefInitialized)
{
gInsertSeparatorPrefInitialized = true;
const char* prefName = "intl.menuitems.insertseparatorbeforeaccesskeys";
nsAutoString val;
Preferences::GetLocalizedString(prefName, val);
gInsertSeparatorBeforeAccessKey = val.EqualsLiteral("true");
}
return gInsertSeparatorBeforeAccessKey;
}
class nsAsyncAccesskeyUpdate final : public nsIReflowCallback
{
public:
explicit nsAsyncAccesskeyUpdate(nsIFrame* aFrame) : mWeakFrame(aFrame)
{
}
virtual bool ReflowFinished() override
{
bool shouldFlush = false;
nsTextBoxFrame* frame =
static_cast<nsTextBoxFrame*>(mWeakFrame.GetFrame());
if (frame) {
shouldFlush = frame->UpdateAccesskey(mWeakFrame);
}
delete this;
return shouldFlush;
}
virtual void ReflowCallbackCanceled() override
{
delete this;
}
WeakFrame mWeakFrame;
};
bool
nsTextBoxFrame::UpdateAccesskey(WeakFrame& aWeakThis)
{
nsAutoString accesskey;
nsCOMPtr<nsIDOMXULLabelElement> labelElement = do_QueryInterface(mContent);
NS_ENSURE_TRUE(aWeakThis.IsAlive(), false);
if (labelElement) {
// Accesskey may be stored on control.
labelElement->GetAccessKey(accesskey);
NS_ENSURE_TRUE(aWeakThis.IsAlive(), false);
} else {
mContent->AsElement()->GetAttr(kNameSpaceID_None,
nsGkAtoms::accesskey,
accesskey);
}
if (!accesskey.Equals(mAccessKey)) {
// Need to get clean mTitle.
RecomputeTitle();
mAccessKey = accesskey;
UpdateAccessTitle();
PresShell()->
FrameNeedsReflow(this, nsIPresShell::eStyleChange,
NS_FRAME_IS_DIRTY);
return true;
}
return false;
}
void
nsTextBoxFrame::UpdateAttributes(nsAtom* aAttribute,
bool& aResize,
bool& aRedraw)
{
bool doUpdateTitle = false;
aResize = false;
aRedraw = false;
if (aAttribute == nullptr || aAttribute == nsGkAtoms::crop) {
static Element::AttrValuesArray strings[] =
{&nsGkAtoms::left, &nsGkAtoms::start, &nsGkAtoms::center,
&nsGkAtoms::right, &nsGkAtoms::end, &nsGkAtoms::none, nullptr};
CroppingStyle cropType;
switch (mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None,
nsGkAtoms::crop, strings,
eCaseMatters)) {
case 0:
case 1:
cropType = CropLeft;
break;
case 2:
cropType = CropCenter;
break;
case 3:
case 4:
cropType = CropRight;
break;
case 5:
cropType = CropNone;
break;
default:
cropType = CropAuto;
break;
}
if (cropType != mCropType) {
aResize = true;
mCropType = cropType;
}
}
if (aAttribute == nullptr || aAttribute == nsGkAtoms::value) {
RecomputeTitle();
doUpdateTitle = true;
}
if (aAttribute == nullptr || aAttribute == nsGkAtoms::accesskey) {
mNeedsReflowCallback = true;
// Ensure that layout is refreshed and reflow callback called.
aResize = true;
}
if (doUpdateTitle) {
UpdateAccessTitle();
aResize = true;
}
}
class nsDisplayXULTextBox final : public nsDisplayItem
{
public:
nsDisplayXULTextBox(nsDisplayListBuilder* aBuilder, nsTextBoxFrame* aFrame)
: nsDisplayItem(aBuilder, aFrame)
{
MOZ_COUNT_CTOR(nsDisplayXULTextBox);
}
#ifdef NS_BUILD_REFCNT_LOGGING
virtual ~nsDisplayXULTextBox() {
MOZ_COUNT_DTOR(nsDisplayXULTextBox);
}
#endif
virtual void Paint(nsDisplayListBuilder* aBuilder,
gfxContext* aCtx) override;
virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
bool* aSnap) const override;
NS_DISPLAY_DECL_NAME("XULTextBox", TYPE_XUL_TEXT_BOX)
virtual nsRect GetComponentAlphaBounds(nsDisplayListBuilder* aBuilder) const override;
void PaintTextToContext(gfxContext* aCtx,
nsPoint aOffset,
const nscolor* aColor);
};
static void
PaintTextShadowCallback(gfxContext* aCtx,
nsPoint aShadowOffset,
const nscolor& aShadowColor,
void* aData)
{
reinterpret_cast<nsDisplayXULTextBox*>(aData)->
PaintTextToContext(aCtx, aShadowOffset, &aShadowColor);
}
void
nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder,
gfxContext* aCtx)
{
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
mDisableSubpixelAA);
// Paint the text shadow before doing any foreground stuff
nsRect drawRect = static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect +
ToReferenceFrame();
nsLayoutUtils::PaintTextShadow(mFrame, aCtx,
drawRect, GetPaintRect(),
mFrame->StyleColor()->mColor,
PaintTextShadowCallback,
(void*)this);
PaintTextToContext(aCtx, nsPoint(0, 0), nullptr);
}
void
nsDisplayXULTextBox::PaintTextToContext(gfxContext* aCtx,
nsPoint aOffset,
const nscolor* aColor)
{
static_cast<nsTextBoxFrame*>(mFrame)->
PaintTitle(*aCtx, GetPaintRect(), ToReferenceFrame() + aOffset, aColor);
}
nsRect
nsDisplayXULTextBox::GetBounds(nsDisplayListBuilder* aBuilder,
bool* aSnap) const
{
*aSnap = false;
return mFrame->GetVisualOverflowRectRelativeToSelf() + ToReferenceFrame();
}
nsRect
nsDisplayXULTextBox::GetComponentAlphaBounds(nsDisplayListBuilder* aBuilder) const
{
return static_cast<nsTextBoxFrame*>(mFrame)->GetComponentAlphaBounds() +
ToReferenceFrame();
}
void
nsTextBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsDisplayListSet& aLists)
{
if (!IsVisibleForPainting(aBuilder))
return;
nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists);
aLists.Content()->AppendToTop(
MakeDisplayItem<nsDisplayXULTextBox>(aBuilder, this));
}
void
nsTextBoxFrame::PaintTitle(gfxContext& aRenderingContext,
const nsRect& aDirtyRect,
nsPoint aPt,
const nscolor* aOverrideColor)
{
if (mTitle.IsEmpty())
return;
DrawText(aRenderingContext, aDirtyRect, mTextDrawRect + aPt, aOverrideColor);
}
void
nsTextBoxFrame::DrawText(gfxContext& aRenderingContext,
const nsRect& aDirtyRect,
const nsRect& aTextRect,
const nscolor* aOverrideColor)
{
nsPresContext* presContext = PresContext();
int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
// paint the title
nscolor overColor = 0;
nscolor underColor = 0;
nscolor strikeColor = 0;
uint8_t overStyle = 0;
uint8_t underStyle = 0;
uint8_t strikeStyle = 0;
// Begin with no decorations
uint8_t decorations = NS_STYLE_TEXT_DECORATION_LINE_NONE;
// A mask of all possible decorations.
uint8_t decorMask = NS_STYLE_TEXT_DECORATION_LINE_LINES_MASK;
WritingMode wm = GetWritingMode();
bool vertical = wm.IsVertical();
nsIFrame* f = this;
do { // find decoration colors
ComputedStyle* context = f->Style();
if (!context->HasTextDecorationLines()) {
break;
}
const nsStyleTextReset* styleText = context->StyleTextReset();
if (decorMask & styleText->mTextDecorationLine) { // a decoration defined here
nscolor color;
if (aOverrideColor) {
color = *aOverrideColor;
} else {
color = styleText->mTextDecorationColor.CalcColor(context);
}
uint8_t style = styleText->mTextDecorationStyle;
if (NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE & decorMask &
styleText->mTextDecorationLine) {
underColor = color;
underStyle = style;
decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
decorations |= NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
}
if (NS_STYLE_TEXT_DECORATION_LINE_OVERLINE & decorMask &
styleText->mTextDecorationLine) {
overColor = color;
overStyle = style;
decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
decorations |= NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
}
if (NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH & decorMask &
styleText->mTextDecorationLine) {
strikeColor = color;
strikeStyle = style;
decorMask &= ~NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
decorations |= NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
}
}
} while (0 != decorMask &&
(f = nsLayoutUtils::GetParentOrPlaceholderFor(f)));
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
fontMet->SetVertical(wm.IsVertical());
fontMet->SetTextOrientation(StyleVisibility()->mTextOrientation);
nscoord offset;
nscoord size;
nscoord ascent = fontMet->MaxAscent();
nsPoint baselinePt;
if (wm.IsVertical()) {
baselinePt.x =
presContext->RoundAppUnitsToNearestDevPixels(aTextRect.x +
(wm.IsVerticalRL() ? aTextRect.width - ascent : ascent));
baselinePt.y = aTextRect.y;
} else {
baselinePt.x = aTextRect.x;
baselinePt.y =
presContext->RoundAppUnitsToNearestDevPixels(aTextRect.y + ascent);
}
nsCSSRendering::PaintDecorationLineParams params;
params.dirtyRect = ToRect(presContext->AppUnitsToGfxUnits(aDirtyRect));
params.pt = Point(presContext->AppUnitsToGfxUnits(aTextRect.x),
presContext->AppUnitsToGfxUnits(aTextRect.y));
params.icoordInFrame =
Float(PresContext()->AppUnitsToGfxUnits(mTextDrawRect.x));
params.lineSize = Size(presContext->AppUnitsToGfxUnits(aTextRect.width), 0);
params.ascent = presContext->AppUnitsToGfxUnits(ascent);
params.vertical = vertical;
// XXX todo: vertical-mode support for decorations not tested yet,
// probably won't be positioned correctly
// Underlines are drawn before overlines, and both before the text
// itself, per http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
// (We don't apply this rule to the access-key underline because we only
// find out where that is as a side effect of drawing the text, in the
// general case -- see below.)
if (decorations & (NS_STYLE_TEXT_DECORATION_LINE_OVERLINE |
NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE)) {
fontMet->GetUnderline(offset, size);
params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE) &&
underStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
params.color = underColor;
params.offset = presContext->AppUnitsToGfxUnits(offset);
params.decoration = NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE;
params.style = underStyle;
nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
}
if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_OVERLINE) &&
overStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
params.color = overColor;
params.offset = params.ascent;
params.decoration = NS_STYLE_TEXT_DECORATION_LINE_OVERLINE;
params.style = overStyle;
nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
}
}
RefPtr<gfxContext> refContext =
PresShell()->CreateReferenceRenderingContext();
DrawTarget* refDrawTarget = refContext->GetDrawTarget();
CalculateUnderline(refDrawTarget, *fontMet);
nscolor c = aOverrideColor ? *aOverrideColor : StyleColor()->mColor;
ColorPattern color(ToDeviceColor(c));
aRenderingContext.SetColor(Color::FromABGR(c));
nsresult rv = NS_ERROR_FAILURE;
if (mState & NS_FRAME_IS_BIDI) {
presContext->SetBidiEnabled();
nsBidiLevel level = nsBidiPresUtils::BidiLevelFromStyle(Style());
if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
// We let the RenderText function calculate the mnemonic's
// underline position for us.
nsBidiPositionResolve posResolve;
posResolve.logicalIndex = mAccessKeyInfo->mAccesskeyIndex;
rv = nsBidiPresUtils::RenderText(mCroppedTitle.get(), mCroppedTitle.Length(), level,
presContext, aRenderingContext,
refDrawTarget, *fontMet,
baselinePt.x, baselinePt.y,
&posResolve,
1);
mAccessKeyInfo->mBeforeWidth = posResolve.visualLeftTwips;
mAccessKeyInfo->mAccessWidth = posResolve.visualWidth;
}
else
{
rv = nsBidiPresUtils::RenderText(mCroppedTitle.get(), mCroppedTitle.Length(), level,
presContext, aRenderingContext,
refDrawTarget, *fontMet,
baselinePt.x, baselinePt.y);
}
}
if (NS_FAILED(rv)) {
fontMet->SetTextRunRTL(false);
if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
// In the simple (non-BiDi) case, we calculate the mnemonic's
// underline position by getting the text metric.
// XXX are attribute values always two byte?
if (mAccessKeyInfo->mAccesskeyIndex > 0)
mAccessKeyInfo->mBeforeWidth = nsLayoutUtils::
AppUnitWidthOfString(mCroppedTitle.get(),
mAccessKeyInfo->mAccesskeyIndex,
*fontMet, refDrawTarget);
else
mAccessKeyInfo->mBeforeWidth = 0;
}
fontMet->DrawString(mCroppedTitle.get(), mCroppedTitle.Length(),
baselinePt.x, baselinePt.y, &aRenderingContext,
refDrawTarget);
}
if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
nsRect r(aTextRect.x + mAccessKeyInfo->mBeforeWidth,
aTextRect.y + mAccessKeyInfo->mAccessOffset,
mAccessKeyInfo->mAccessWidth,
mAccessKeyInfo->mAccessUnderlineSize);
Rect devPxRect =
NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
drawTarget->FillRect(devPxRect, color);
}
// Strikeout is drawn on top of the text, per
// http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
if ((decorations & NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH) &&
strikeStyle != NS_STYLE_TEXT_DECORATION_STYLE_NONE) {
fontMet->GetStrikeout(offset, size);
params.color = strikeColor;
params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
params.offset = presContext->AppUnitsToGfxUnits(offset);
params.decoration = NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH;
params.style = strikeStyle;
nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
}
}
void
nsTextBoxFrame::CalculateUnderline(DrawTarget* aDrawTarget,
nsFontMetrics& aFontMetrics)
{
if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
// Calculate all fields of mAccessKeyInfo which
// are the same for both BiDi and non-BiDi frames.
const char16_t *titleString = mCroppedTitle.get();
aFontMetrics.SetTextRunRTL(false);
mAccessKeyInfo->mAccessWidth = nsLayoutUtils::
AppUnitWidthOfString(titleString[mAccessKeyInfo->mAccesskeyIndex],
aFontMetrics, aDrawTarget);
nscoord offset, baseline;
aFontMetrics.GetUnderline(offset, mAccessKeyInfo->mAccessUnderlineSize);
baseline = aFontMetrics.MaxAscent();
mAccessKeyInfo->mAccessOffset = baseline - offset;
}
}
nscoord
nsTextBoxFrame::CalculateTitleForWidth(gfxContext& aRenderingContext,
nscoord aWidth)
{
DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
if (mTitle.IsEmpty()) {
mCroppedTitle.Truncate();
return 0;
}
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
// see if the text will completely fit in the width given
nscoord titleWidth =
nsLayoutUtils::AppUnitWidthOfStringBidi(mTitle, this, *fm,
aRenderingContext);
if (titleWidth <= aWidth) {
mCroppedTitle = mTitle;
if (HasRTLChars(mTitle) ||
StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) {
AddStateBits(NS_FRAME_IS_BIDI);
}
return titleWidth; // fits, done.
}
const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
if (mCropType != CropNone) {
// start with an ellipsis
mCroppedTitle.Assign(kEllipsis);
// see if the width is even smaller than the ellipsis
// if so, clear the text (XXX set as many '.' as we can?).
fm->SetTextRunRTL(false);
titleWidth = nsLayoutUtils::AppUnitWidthOfString(kEllipsis, *fm,
drawTarget);
if (titleWidth > aWidth) {
mCroppedTitle.SetLength(0);
return 0;
}
// if the ellipsis fits perfectly, no use in trying to insert
if (titleWidth == aWidth)
return titleWidth;
aWidth -= titleWidth;
} else {
mCroppedTitle.Truncate(0);
titleWidth = 0;
}
using mozilla::unicode::ClusterIterator;
using mozilla::unicode::ClusterReverseIterator;
// ok crop things
switch (mCropType)
{
case CropAuto:
case CropNone:
case CropRight:
{
ClusterIterator iter(mTitle.Data(), mTitle.Length());
const char16_t* dataBegin = iter;
const char16_t* pos = dataBegin;
nscoord charWidth;
nscoord totalWidth = 0;
while (!iter.AtEnd()) {
iter.Next();
const char16_t* nextPos = iter;
ptrdiff_t length = nextPos - pos;
charWidth = nsLayoutUtils::AppUnitWidthOfString(pos, length,
*fm,
drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
AddStateBits(NS_FRAME_IS_BIDI);
}
pos = nextPos;
totalWidth += charWidth;
}
if (pos == dataBegin) {
return titleWidth;
}
// insert what character we can in.
nsAutoString title(mTitle);
title.Truncate(pos - dataBegin);
mCroppedTitle.Insert(title, 0);
}
break;
case CropLeft:
{
ClusterReverseIterator iter(mTitle.Data(), mTitle.Length());
const char16_t* dataEnd = iter;
const char16_t* prevPos = dataEnd;
nscoord charWidth;
nscoord totalWidth = 0;
while (!iter.AtEnd()) {
iter.Next();
const char16_t* pos = iter;
ptrdiff_t length = prevPos - pos;
charWidth = nsLayoutUtils::AppUnitWidthOfString(pos, length,
*fm,
drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
AddStateBits(NS_FRAME_IS_BIDI);
}
prevPos = pos;
totalWidth += charWidth;
}
if (prevPos == dataEnd) {
return titleWidth;
}
nsAutoString copy;
mTitle.Right(copy, dataEnd - prevPos);
mCroppedTitle += copy;
}
break;
case CropCenter:
{
nscoord stringWidth =
nsLayoutUtils::AppUnitWidthOfStringBidi(mTitle, this, *fm,
aRenderingContext);
if (stringWidth <= aWidth) {
// the entire string will fit in the maximum width
mCroppedTitle.Insert(mTitle, 0);
break;
}
// determine how much of the string will fit in the max width
nscoord charWidth = 0;
nscoord totalWidth = 0;
ClusterIterator leftIter(mTitle.Data(), mTitle.Length());
ClusterReverseIterator rightIter(mTitle.Data(), mTitle.Length());
const char16_t* dataBegin = leftIter;
const char16_t* dataEnd = rightIter;
const char16_t* leftPos = dataBegin;
const char16_t* rightPos = dataEnd;
const char16_t* pos;
ptrdiff_t length;
nsAutoString leftString, rightString;
while (leftPos < rightPos) {
leftIter.Next();
pos = leftIter;
length = pos - leftPos;
charWidth = nsLayoutUtils::AppUnitWidthOfString(leftPos, length,
*fm,
drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(*leftPos)) {
AddStateBits(NS_FRAME_IS_BIDI);
}
leftString.Append(leftPos, length);
leftPos = pos;
totalWidth += charWidth;
if (leftPos >= rightPos) {
break;
}
rightIter.Next();
pos = rightIter;
length = rightPos - pos;
charWidth = nsLayoutUtils::AppUnitWidthOfString(pos, length,
*fm,
drawTarget);
if (totalWidth + charWidth > aWidth) {
break;
}
if (UTF16_CODE_UNIT_IS_BIDI(*pos)) {
AddStateBits(NS_FRAME_IS_BIDI);
}
rightString.Insert(pos, 0, length);
rightPos = pos;
totalWidth += charWidth;
}
mCroppedTitle = leftString + kEllipsis + rightString;
}
break;
}
return nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm,
aRenderingContext);
}
#define OLD_ELLIPSIS NS_LITERAL_STRING("...")
// the following block is to append the accesskey to mTitle if there is an accesskey
// but the mTitle doesn't have the character
void
nsTextBoxFrame::UpdateAccessTitle()
{
/*
* Note that if you change appending access key label spec,
* you need to maintain same logic in following methods. See bug 324159.
* toolkit/components/prompts/src/CommonDialog.jsm (setLabelForNode)
* toolkit/content/widgets/text.xml (formatAccessKey)
*/
int32_t menuAccessKey;
nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
if (!menuAccessKey || mAccessKey.IsEmpty())
return;
if (!AlwaysAppendAccessKey() &&
FindInReadable(mAccessKey, mTitle, nsCaseInsensitiveStringComparator()))
return;
nsAutoString accessKeyLabel;
accessKeyLabel += '(';
accessKeyLabel += mAccessKey;
ToUpperCase(accessKeyLabel);
accessKeyLabel += ')';
if (mTitle.IsEmpty()) {
mTitle = accessKeyLabel;
return;
}
if (StringEndsWith(mTitle, accessKeyLabel)) {
// Never append another "(X)" if the title already ends with "(X)".
return;
}
const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
uint32_t offset = mTitle.Length();
if (StringEndsWith(mTitle, kEllipsis)) {
offset -= kEllipsis.Length();
} else if (StringEndsWith(mTitle, OLD_ELLIPSIS)) {
// Try to check with our old ellipsis (for old addons)
offset -= OLD_ELLIPSIS.Length();
} else {
// Try to check with
// our default ellipsis (for non-localized addons) or ':'
const char16_t kLastChar = mTitle.Last();
if (kLastChar == char16_t(0x2026) || kLastChar == char16_t(':'))
offset--;
}
if (InsertSeparatorBeforeAccessKey() &&
offset > 0 && !NS_IS_SPACE(mTitle[offset - 1])) {
mTitle.Insert(' ', offset);
offset++;
}
mTitle.Insert(accessKeyLabel, offset);
}
void
nsTextBoxFrame::UpdateAccessIndex()
{
int32_t menuAccessKey;
nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
if (menuAccessKey) {
if (mAccessKey.IsEmpty()) {
if (mAccessKeyInfo) {
delete mAccessKeyInfo;
mAccessKeyInfo = nullptr;
}
} else {
if (!mAccessKeyInfo) {
mAccessKeyInfo = new nsAccessKeyInfo();
if (!mAccessKeyInfo)
return;
}
nsAString::const_iterator start, end;
mCroppedTitle.BeginReading(start);
mCroppedTitle.EndReading(end);
// remember the beginning of the string
nsAString::const_iterator originalStart = start;
bool found;
if (!AlwaysAppendAccessKey()) {
// not appending access key - do case-sensitive search
// first
found = FindInReadable(mAccessKey, start, end);
if (!found) {
// didn't find it - perform a case-insensitive search
start = originalStart;
found = FindInReadable(mAccessKey, start, end,
nsCaseInsensitiveStringComparator());
}
} else {
found = RFindInReadable(mAccessKey, start, end,
nsCaseInsensitiveStringComparator());
}
if (found)
mAccessKeyInfo->mAccesskeyIndex = Distance(originalStart, start);
else
mAccessKeyInfo->mAccesskeyIndex = kNotFound;
}
}
}
void
nsTextBoxFrame::RecomputeTitle()
{
mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, mTitle);
// This doesn't handle language-specific uppercasing/lowercasing
// rules, unlike textruns.
uint8_t textTransform = StyleText()->mTextTransform;
if (textTransform == NS_STYLE_TEXT_TRANSFORM_UPPERCASE) {
ToUpperCase(mTitle);
} else if (textTransform == NS_STYLE_TEXT_TRANSFORM_LOWERCASE) {
ToLowerCase(mTitle);
}
// We can't handle NS_STYLE_TEXT_TRANSFORM_CAPITALIZE because we
// have no clue about word boundaries here. We also don't handle
// NS_STYLE_TEXT_TRANSFORM_FULL_WIDTH.
}
void
nsTextBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle)
{
if (!aOldComputedStyle) {
// We're just being initialized
return;
}
const nsStyleText* oldTextStyle = aOldComputedStyle->PeekStyleText();
// We should really have oldTextStyle here, since we asked for our
// nsStyleText during Init(), but if it's not there for some reason
// just assume the worst and recompute mTitle.
if (!oldTextStyle ||
oldTextStyle->mTextTransform != StyleText()->mTextTransform) {
RecomputeTitle();
UpdateAccessTitle();
}
}
NS_IMETHODIMP
nsTextBoxFrame::DoXULLayout(nsBoxLayoutState& aBoxLayoutState)
{
if (mNeedsReflowCallback) {
nsIReflowCallback* cb = new nsAsyncAccesskeyUpdate(this);
if (cb) {
PresShell()->PostReflowCallback(cb);
}
mNeedsReflowCallback = false;
}
nsresult rv = nsLeafBoxFrame::DoXULLayout(aBoxLayoutState);
CalcDrawRect(*aBoxLayoutState.GetRenderingContext());
const nsStyleText* textStyle = StyleText();
nsRect scrollBounds(nsPoint(0, 0), GetSize());
nsRect textRect = mTextDrawRect;
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
nsBoundingMetrics metrics =
fontMet->GetInkBoundsForVisualOverflow(mCroppedTitle.get(),
mCroppedTitle.Length(),
aBoxLayoutState.GetRenderingContext()->GetDrawTarget());
WritingMode wm = GetWritingMode();
LogicalRect tr(wm, textRect, GetSize());
tr.IStart(wm) -= metrics.leftBearing;
tr.ISize(wm) = metrics.width;
// In DrawText() we always draw with the baseline at MaxAscent() (relative to mTextDrawRect),
tr.BStart(wm) += fontMet->MaxAscent() - metrics.ascent;
tr.BSize(wm) = metrics.ascent + metrics.descent;
textRect = tr.GetPhysicalRect(wm, GetSize());
// Our scrollable overflow is our bounds; our visual overflow may
// extend beyond that.
nsRect visualBounds;
visualBounds.UnionRect(scrollBounds, textRect);
nsOverflowAreas overflow(visualBounds, scrollBounds);
if (textStyle->mTextShadow) {
// text-shadow extends our visual but not scrollable bounds
nsRect &vis = overflow.VisualOverflow();
vis.UnionRect(vis, nsLayoutUtils::GetTextShadowRectsUnion(mTextDrawRect, this));
}
FinishAndStoreOverflow(overflow, GetSize());
return rv;
}
nsRect
nsTextBoxFrame::GetComponentAlphaBounds() const
{
if (StyleText()->mTextShadow) {
return GetVisualOverflowRectRelativeToSelf();
}
return mTextDrawRect;
}
bool
nsTextBoxFrame::ComputesOwnOverflowArea()
{
return true;
}
/* virtual */ void
nsTextBoxFrame::MarkIntrinsicISizesDirty()
{
mNeedsRecalc = true;
nsLeafBoxFrame::MarkIntrinsicISizesDirty();
}
void
nsTextBoxFrame::GetTextSize(gfxContext& aRenderingContext,
const nsString& aString,
nsSize& aSize, nscoord& aAscent)
{
RefPtr<nsFontMetrics> fontMet =
nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
aSize.height = fontMet->MaxHeight();
aSize.width =
nsLayoutUtils::AppUnitWidthOfStringBidi(aString, this, *fontMet,
aRenderingContext);
aAscent = fontMet->MaxAscent();
}
void
nsTextBoxFrame::CalcTextSize(nsBoxLayoutState& aBoxLayoutState)
{
if (mNeedsRecalc) {
nsSize size;
gfxContext* rendContext = aBoxLayoutState.GetRenderingContext();
if (rendContext) {
GetTextSize(*rendContext, mTitle, size, mAscent);
if (GetWritingMode().IsVertical()) {
Swap(size.width, size.height);
}
mTextSize = size;
mNeedsRecalc = false;
}
}
}
void
nsTextBoxFrame::CalcDrawRect(gfxContext &aRenderingContext)
{
WritingMode wm = GetWritingMode();
LogicalRect textRect(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm));
nsMargin borderPadding;
GetXULBorderAndPadding(borderPadding);
textRect.Deflate(wm, LogicalMargin(wm, borderPadding));
// determine (cropped) title and underline position
// determine (cropped) title which fits in aRect, and its width
// (where "width" is the text measure along its baseline, i.e. actually
// a physical height in vertical writing modes)
nscoord titleWidth =
CalculateTitleForWidth(aRenderingContext, textRect.ISize(wm));
#ifdef ACCESSIBILITY
// Make sure to update the accessible tree in case when cropped title is
// changed.
nsAccessibilityService* accService = GetAccService();
if (accService) {
accService->UpdateLabelValue(PresShell(), mContent,
mCroppedTitle);
}
#endif
// determine if and at which position to put the underline
UpdateAccessIndex();
// make the rect as small as our (cropped) text.
nscoord outerISize = textRect.ISize(wm);
textRect.ISize(wm) = titleWidth;
// Align our text within the overall rect by checking our text-align property.
const nsStyleText* textStyle = StyleText();
if (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_CENTER) {
textRect.IStart(wm) += (outerISize - textRect.ISize(wm)) / 2;
} else if (textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_END ||
(textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_LEFT &&
!wm.IsBidiLTR()) ||
(textStyle->mTextAlign == NS_STYLE_TEXT_ALIGN_RIGHT &&
wm.IsBidiLTR())) {
textRect.IStart(wm) += (outerISize - textRect.ISize(wm));
}
mTextDrawRect = textRect.GetPhysicalRect(wm, GetSize());
}
/**
* Ok return our dimensions
*/
nsSize
nsTextBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState)
{
CalcTextSize(aBoxLayoutState);
nsSize size = mTextSize;
DISPLAY_PREF_SIZE(this, size);
AddBorderAndPadding(size);
bool widthSet, heightSet;
nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet);
return size;
}
/**
* Ok return our dimensions
*/
nsSize
nsTextBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState)
{
CalcTextSize(aBoxLayoutState);
nsSize size = mTextSize;
DISPLAY_MIN_SIZE(this, size);
// if there is cropping our min width becomes our border and padding
if (mCropType != CropNone && mCropType != CropAuto) {
if (GetWritingMode().IsVertical()) {
size.height = 0;
} else {
size.width = 0;
}
}
AddBorderAndPadding(size);
bool widthSet, heightSet;
nsIFrame::AddXULMinSize(aBoxLayoutState, this, size, widthSet, heightSet);
return size;
}
nscoord
nsTextBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState)
{
CalcTextSize(aBoxLayoutState);
nscoord ascent = mAscent;
nsMargin m(0,0,0,0);
GetXULBorderAndPadding(m);
WritingMode wm = GetWritingMode();
ascent += LogicalMargin(wm, m).BStart(wm);
return ascent;
}
#ifdef DEBUG_FRAME_DUMP
nsresult
nsTextBoxFrame::GetFrameName(nsAString& aResult) const
{
MakeFrameName(NS_LITERAL_STRING("TextBox"), aResult);
aResult += NS_LITERAL_STRING("[value=") + mTitle + NS_LITERAL_STRING("]");
return NS_OK;
}
#endif
// If you make changes to this function, check its counterparts
// in nsBoxFrame and nsXULLabelFrame
nsresult
nsTextBoxFrame::RegUnregAccessKey(bool aDoReg)
{
// if we have no content, we can't do anything
if (!mContent)
return NS_ERROR_FAILURE;
// check if we have a |control| attribute
// do this check first because few elements have control attributes, and we
// can weed out most of the elements quickly.
// XXXjag a side-effect is that we filter out anonymous <label>s
// in e.g. <menu>, <menuitem>, <button>. These <label>s inherit
// |accesskey| and would otherwise register themselves, overwriting
// the content we really meant to be registered.
if (!mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::control))
return NS_OK;
// see if we even have an access key
nsAutoString accessKey;
mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
accessKey);
if (accessKey.IsEmpty())
return NS_OK;
// With a valid PresContext we can get the ESM
// and (un)register the access key
EventStateManager* esm = PresContext()->EventStateManager();
uint32_t key = accessKey.First();
if (aDoReg)
esm->RegisterAccessKey(mContent->AsElement(), key);
else
esm->UnregisterAccessKey(mContent->AsElement(), key);
return NS_OK;
}