Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "Accessible.h"
#include "ARIAMap.h"
#include "nsAccUtils.h"
#include "nsIURI.h"
#include "Relation.h"
#include "States.h"
#include "mozilla/a11y/FocusManager.h"
#include "mozilla/a11y/HyperTextAccessibleBase.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/Components.h"
#include "nsIStringBundle.h"
#ifdef A11Y_LOG
# include "nsAccessibilityService.h"
#endif
using namespace mozilla;
using namespace mozilla::a11y;
Accessible::Accessible()
: mType(static_cast<uint32_t>(0)),
mGenericTypes(static_cast<uint32_t>(0)),
mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {}
Accessible::Accessible(AccType aType, AccGenericType aGenericTypes,
uint8_t aRoleMapEntryIndex)
: mType(static_cast<uint32_t>(aType)),
mGenericTypes(static_cast<uint32_t>(aGenericTypes)),
mRoleMapEntryIndex(aRoleMapEntryIndex) {}
void Accessible::StaticAsserts() const {
static_assert(eLastAccType <= (1 << kTypeBits) - 1,
"Accessible::mType was oversized by eLastAccType!");
static_assert(
eLastAccGenericType <= (1 << kGenericTypesBits) - 1,
"Accessible::mGenericType was oversized by eLastAccGenericType!");
}
bool Accessible::IsBefore(const Accessible* aAcc) const {
// Build the chain of parents.
const Accessible* thisP = this;
const Accessible* otherP = aAcc;
AutoTArray<const Accessible*, 30> thisParents, otherParents;
do {
thisParents.AppendElement(thisP);
thisP = thisP->Parent();
} while (thisP);
do {
otherParents.AppendElement(otherP);
otherP = otherP->Parent();
} while (otherP);
// Find where the parent chain differs.
uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length();
for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) {
const Accessible* thisChild = thisParents.ElementAt(--thisPos);
const Accessible* otherChild = otherParents.ElementAt(--otherPos);
if (thisChild != otherChild) {
return thisChild->IndexInParent() < otherChild->IndexInParent();
}
}
// If the ancestries are the same length (both thisPos and otherPos are 0),
// we should have returned by now.
MOZ_ASSERT(thisPos != 0 || otherPos != 0);
// At this point, one of the ancestries is a superset of the other, so one of
// thisPos or otherPos should be 0.
MOZ_ASSERT(thisPos != otherPos);
// If the other Accessible is deeper than this one (otherPos > 0), this
// Accessible comes before the other.
return otherPos > 0;
}
const Accessible* Accessible::GetClosestCommonInclusiveAncestor(
const Accessible* aAcc) const {
if (aAcc == this) {
return this;
}
// Build the chain of parents.
const Accessible* thisAnc = this;
const Accessible* otherAnc = aAcc;
AutoTArray<const Accessible*, 30> thisAncs, otherAncs;
do {
thisAncs.AppendElement(thisAnc);
thisAnc = thisAnc->Parent();
} while (thisAnc);
do {
otherAncs.AppendElement(otherAnc);
otherAnc = otherAnc->Parent();
} while (otherAnc);
// Find where the parent chain differs.
size_t thisPos = thisAncs.Length(), otherPos = otherAncs.Length();
const Accessible* common = nullptr;
for (size_t len = std::min(thisPos, otherPos); len > 0; --len) {
const Accessible* thisChild = thisAncs.ElementAt(--thisPos);
const Accessible* otherChild = otherAncs.ElementAt(--otherPos);
if (thisChild != otherChild) {
break;
}
common = thisChild;
}
return common;
}
Accessible* Accessible::FocusedChild() {
Accessible* doc = nsAccUtils::DocumentFor(this);
Accessible* child = doc->FocusedChild();
if (child && (child == this || child->Parent() == this)) {
return child;
}
return nullptr;
}
const nsRoleMapEntry* Accessible::ARIARoleMap() const {
return aria::GetRoleMapFromIndex(mRoleMapEntryIndex);
}
bool Accessible::HasARIARole() const {
return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX;
}
bool Accessible::IsARIARole(nsAtom* aARIARole) const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
return roleMapEntry && roleMapEntry->Is(aARIARole);
}
bool Accessible::HasStrongARIARole() const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
return roleMapEntry && roleMapEntry->roleRule == kUseMapRole;
}
bool Accessible::HasGenericType(AccGenericType aType) const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
return (mGenericTypes & aType) ||
(roleMapEntry && roleMapEntry->IsOfType(aType));
}
nsIntRect Accessible::BoundsInCSSPixels() const {
return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel());
}
LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); }
LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) {
LayoutDeviceIntPoint point = Bounds().TopLeft();
nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
this);
return point;
}
bool Accessible::IsTextRole() {
if (!IsHyperText()) {
return false;
}
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC ||
roleMapEntry->role == roles::IMAGE_MAP ||
roleMapEntry->role == roles::SLIDER ||
roleMapEntry->role == roles::PROGRESSBAR ||
roleMapEntry->role == roles::SEPARATOR)) {
return false;
}
return true;
}
uint32_t Accessible::StartOffset() {
MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!");
Accessible* parent = Parent();
HyperTextAccessibleBase* hyperText =
parent ? parent->AsHyperTextBase() : nullptr;
return hyperText ? hyperText->GetChildOffset(this) : 0;
}
uint32_t Accessible::EndOffset() {
MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!");
Accessible* parent = Parent();
HyperTextAccessibleBase* hyperText =
parent ? parent->AsHyperTextBase() : nullptr;
return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0;
}
GroupPos Accessible::GroupPosition() {
GroupPos groupPos;
// Try aria-row/colcount/index.
if (IsTableRow()) {
Accessible* table = nsAccUtils::TableFor(this);
if (table) {
if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) {
if (*count >= 0) {
groupPos.setSize = *count;
}
}
}
if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) {
groupPos.posInSet = *index;
}
if (groupPos.setSize && groupPos.posInSet) {
return groupPos;
}
}
if (IsTableCell()) {
Accessible* table;
for (table = Parent(); table; table = table->Parent()) {
if (table->IsTable()) {
break;
}
}
if (table) {
if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) {
if (*count >= 0) {
groupPos.setSize = *count;
}
}
}
if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) {
groupPos.posInSet = *index;
}
if (groupPos.setSize && groupPos.posInSet) {
return groupPos;
}
}
// Get group position from ARIA attributes.
ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet);
// If ARIA is missed and the accessible is visible then calculate group
// position from hierarchy.
if (State() & states::INVISIBLE) return groupPos;
// Calculate group level if ARIA is missed.
if (groupPos.level == 0) {
groupPos.level = GetLevel(false);
}
// Calculate position in group and group size if ARIA is missed.
if (groupPos.posInSet == 0 || groupPos.setSize == 0) {
int32_t posInSet = 0, setSize = 0;
GetPositionAndSetSize(&posInSet, &setSize);
if (posInSet != 0 && setSize != 0) {
if (groupPos.posInSet == 0) groupPos.posInSet = posInSet;
if (groupPos.setSize == 0) groupPos.setSize = setSize;
}
}
return groupPos;
}
int32_t Accessible::GetLevel(bool aFast) const {
int32_t level = 0;
if (!Parent()) return level;
roles::Role role = Role();
if (role == roles::OUTLINEITEM) {
// Always expose 'level' attribute for 'outlineitem' accessible. The number
// of nested 'grouping' accessibles containing 'outlineitem' accessible is
// its level.
level = 1;
if (!aFast) {
const Accessible* parent = this;
while ((parent = parent->Parent()) && !parent->IsDoc()) {
roles::Role parentRole = parent->Role();
if (parentRole == roles::OUTLINE) break;
if (parentRole == roles::GROUPING) ++level;
}
}
} else if (role == roles::LISTITEM && !aFast) {
// Expose 'level' attribute on nested lists. We support two hierarchies:
// a) list -> listitem -> list -> listitem (nested list is a last child
// of listitem of the parent list);
// b) list -> listitem -> group -> listitem (nested listitems are contained
// by group that is a last child of the parent listitem).
// Calculate 'level' attribute based on number of parent listitems.
level = 0;
const Accessible* parent = this;
while ((parent = parent->Parent()) && !parent->IsDoc()) {
roles::Role parentRole = parent->Role();
if (parentRole == roles::LISTITEM) {
++level;
} else if (parentRole != roles::LIST && parentRole != roles::GROUPING) {
break;
}
}
if (level == 0) {
// If this listitem is on top of nested lists then expose 'level'
// attribute.
parent = Parent();
uint32_t siblingCount = parent->ChildCount();
for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) {
Accessible* sibling = parent->ChildAt(siblingIdx);
Accessible* siblingChild = sibling->LastChild();
if (siblingChild) {
roles::Role lastChildRole = siblingChild->Role();
if (lastChildRole == roles::LIST ||
lastChildRole == roles::GROUPING) {
return 1;
}
}
}
} else {
++level; // level is 1-index based
}
} else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) {
if (const Accessible* parent = Parent()) {
if (parent->IsHTMLOptGroup()) {
return 2;
}
if (parent->IsListControl() && !parent->ARIARoleMap()) {
// This is for HTML selects only.
if (aFast) {
return 1;
}
for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) {
if (parent->ChildAt(i)->IsHTMLOptGroup()) {
return 1;
}
}
}
}
} else if (role == roles::HEADING) {
nsAtom* tagName = TagName();
if (tagName == nsGkAtoms::h1) {
return 1;
}
if (tagName == nsGkAtoms::h2) {
return 2;
}
if (tagName == nsGkAtoms::h3) {
return 3;
}
if (tagName == nsGkAtoms::h4) {
return 4;
}
if (tagName == nsGkAtoms::h5) {
return 5;
}
if (tagName == nsGkAtoms::h6) {
return 6;
}
const nsRoleMapEntry* ariaRole = this->ARIARoleMap();
if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) {
// An aria heading with no aria level has a default level of 2.
return 2;
}
} else if (role == roles::COMMENT) {
// For comments, count the ancestor elements with the same role to get the
// level.
level = 1;
if (!aFast) {
const Accessible* parent = this;
while ((parent = parent->Parent()) && !parent->IsDoc()) {
roles::Role parentRole = parent->Role();
if (parentRole == roles::COMMENT) {
++level;
}
}
}
} else if (role == roles::ROW) {
// It is a row inside flatten treegrid. Group level is always 1 until it
// is overriden by aria-level attribute.
const Accessible* parent = Parent();
if (parent->Role() == roles::TREE_TABLE) {
return 1;
}
}
return level;
}
void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) {
auto groupInfo = GetOrCreateGroupInfo();
if (groupInfo) {
*aPosInSet = groupInfo->PosInSet();
*aSetSize = groupInfo->SetSize();
}
}
bool Accessible::IsLinkValid() {
MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
// XXX In order to implement this we would need to follow every link
// Perhaps we can get information about invalid links from the cache
// In the mean time authors can use role="link" aria-invalid="true"
// to force it for links they internally know to be invalid
return (0 == (State() & mozilla::a11y::states::INVALID));
}
uint32_t Accessible::AnchorCount() {
if (IsImageMap()) {
return ChildCount();
}
MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
return 1;
}
Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const {
if (IsImageMap()) {
return ChildAt(aAnchorIndex);
}
MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
return aAnchorIndex == 0 ? const_cast<Accessible*>(this) : nullptr;
}
already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const {
Accessible* anchor = nullptr;
if (IsTextLeaf() || IsImage()) {
for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
parent = parent->Parent()) {
if (parent->IsLink()) {
anchor = parent->AnchorAt(aAnchorIndex);
}
}
} else {
anchor = AnchorAt(aAnchorIndex);
}
if (anchor) {
RefPtr<nsIURI> uri;
nsAutoString spec;
anchor->Value(spec);
nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
if (NS_SUCCEEDED(rv)) {
return uri.forget();
}
}
return nullptr;
}
bool Accessible::IsSearchbox() const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) {
return true;
}
RefPtr<nsAtom> inputType = InputType();
return inputType == nsGkAtoms::search;
}
#ifdef A11Y_LOG
void Accessible::DebugDescription(nsCString& aDesc) const {
aDesc.Truncate();
aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local");
aDesc.AppendPrintf("[%p] ", this);
nsAutoString role;
GetAccService()->GetStringRole(Role(), role);
aDesc.Append(NS_ConvertUTF16toUTF8(role));
if (nsAtom* tagAtom = TagName()) {
nsAutoCString tag;
tagAtom->ToUTF8String(tag);
aDesc.AppendPrintf(" %s", tag.get());
nsAutoString id;
DOMNodeID(id);
if (!id.IsEmpty()) {
aDesc.Append("#");
aDesc.Append(NS_ConvertUTF16toUTF8(id));
}
}
nsAutoString id;
nsAutoString name;
Name(name);
if (!name.IsEmpty()) {
aDesc.Append(" '");
aDesc.Append(NS_ConvertUTF16toUTF8(name));
aDesc.Append("'");
}
}
void Accessible::DebugPrint(const char* aPrefix,
const Accessible* aAccessible) {
nsAutoCString desc;
if (aAccessible) {
aAccessible->DebugDescription(desc);
} else {
desc.AssignLiteral("[null]");
}
# if defined(ANDROID) || defined(MOZ_WIDGET_UIKIT)
printf_stderr("%s %s\n", aPrefix, desc.get());
# else
printf("%s %s\n", aPrefix, desc.get());
# endif
}
#endif
void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut,
const nsTArray<nsString>& aParams) {
nsCOMPtr<nsIStringBundleService> stringBundleService =
components::StringBundle::Service();
if (!stringBundleService) return;
nsCOMPtr<nsIStringBundle> stringBundle;
stringBundleService->CreateBundle(
"chrome://global-platform/locale/accessible.properties",
getter_AddRefs(stringBundle));
if (!stringBundle) return;
nsAutoString xsValue;
nsresult rv = NS_OK;
if (aParams.IsEmpty()) {
rv = stringBundle->GetStringFromName(NS_ConvertUTF16toUTF8(aKey).get(),
xsValue);
} else {
rv = stringBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(aKey).get(),
aParams, xsValue);
}
if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue);
}
const Accessible* Accessible::ActionAncestor() const {
// We do want to consider a click handler on the document. However, we don't
// want to walk outside of this document, so we stop if we see an OuterDoc.
for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
parent = parent->Parent()) {
if (parent->HasPrimaryAction()) {
return parent;
}
}
return nullptr;
}
nsStaticAtom* Accessible::LandmarkRole() const {
// For certain cases below (e.g. ARIA region, HTML <header>), whether it is
// actually a landmark is conditional. Rather than duplicating that
// conditional logic here, we check the Gecko role.
if (const nsRoleMapEntry* roleMapEntry = ARIARoleMap()) {
// Explicit ARIA role should take precedence.
if (roleMapEntry->Is(nsGkAtoms::region)) {
if (Role() == roles::REGION) {
return nsGkAtoms::region;
}
} else if (roleMapEntry->Is(nsGkAtoms::form)) {
if (Role() == roles::FORM) {
return nsGkAtoms::form;
}
} else if (roleMapEntry->IsOfType(eLandmark)) {
return roleMapEntry->roleAtom;
}
}
nsAtom* tagName = TagName();
if (!tagName) {
// Either no associated content, or no cache.
return nullptr;
}
if (tagName == nsGkAtoms::nav) {
return nsGkAtoms::navigation;
}
if (tagName == nsGkAtoms::aside) {
return nsGkAtoms::complementary;
}
if (tagName == nsGkAtoms::main) {
return nsGkAtoms::main;
}
if (tagName == nsGkAtoms::header) {
if (Role() == roles::LANDMARK) {
return nsGkAtoms::banner;
}
}
if (tagName == nsGkAtoms::footer) {
if (Role() == roles::LANDMARK) {
return nsGkAtoms::contentinfo;
}
}
if (tagName == nsGkAtoms::section) {
if (Role() == roles::REGION) {
return nsGkAtoms::region;
}
}
if (tagName == nsGkAtoms::form) {
if (Role() == roles::FORM_LANDMARK) {
return nsGkAtoms::form;
}
}
if (tagName == nsGkAtoms::search) {
return nsGkAtoms::search;
}
return nullptr;
}
nsStaticAtom* Accessible::ComputedARIARole() const {
const nsRoleMapEntry* roleMap = ARIARoleMap();
if (roleMap && roleMap->IsOfType(eDPub)) {
return roleMap->roleAtom;
}
if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty &&
// region and form have their own Gecko roles and need to be handled
// specially.
roleMap->roleAtom != nsGkAtoms::region &&
roleMap->roleAtom != nsGkAtoms::form &&
(roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) ||
roleMap->roleAtom == nsGkAtoms::alertdialog ||
roleMap->roleAtom == nsGkAtoms::feed)) {
// Explicit ARIA role (e.g. specified via the role attribute) which does not
// map to a unique Gecko role.
return roleMap->roleAtom;
}
if (IsSearchbox()) {
return nsGkAtoms::searchbox;
}
role geckoRole = Role();
if (geckoRole == roles::LANDMARK) {
// Landmark role from native markup; e.g. <main>, <nav>.
return LandmarkRole();
}
// Role from native markup or layout.
#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
nameRule) \
case roles::_geckoRole: \
return ariaRole;
switch (geckoRole) {
#include "RoleMap.h"
}
#undef ROLE
MOZ_ASSERT_UNREACHABLE("Unknown role");
return nullptr;
}
void Accessible::ApplyImplicitState(uint64_t& aState) const {
// nsAccessibilityService (and thus FocusManager) can be shut down before
// RemoteAccessibles.
if (const auto* focusMgr = FocusMgr()) {
if (focusMgr->IsFocused(this)) {
aState |= states::FOCUSED;
}
}
// If this is an option, tab or treeitem and if it's focused and not marked
// unselected explicitly (i.e. aria-selected="false") then expose it as
// selected to make ARIA widget authors life easier.
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
if (roleMapEntry &&
(roleMapEntry->Is(nsGkAtoms::option) ||
roleMapEntry->Is(nsGkAtoms::tab) ||
roleMapEntry->Is(nsGkAtoms::treeitem)) &&
!(aState & states::SELECTED) && ARIASelected().valueOr(true)) {
// Special case for tabs: focused tab or focus inside related tab panel
// implies selected state.
if (roleMapEntry->role == roles::PAGETAB) {
if (aState & states::FOCUSED) {
aState |= states::SELECTED;
} else {
// If focus is in a child of the tab panel surely the tab is selected!
Relation rel = RelationByType(RelationType::LABEL_FOR);
Accessible* relTarget = nullptr;
while ((relTarget = rel.Next())) {
if (relTarget->Role() == roles::PROPERTYPAGE &&
FocusMgr()->IsFocusWithin(relTarget)) {
aState |= states::SELECTED;
}
}
}
} else if (aState & states::FOCUSED) {
Accessible* container = nsAccUtils::GetSelectableContainer(this, aState);
if (container && !(container->State() & states::MULTISELECTABLE)) {
aState |= states::SELECTED;
}
}
}
if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) {
aState |= states::OPAQUE1;
}
}
bool Accessible::NameIsEmpty() const {
nsAutoString name;
Name(name);
return name.IsEmpty();
}
////////////////////////////////////////////////////////////////////////////////
// KeyBinding class
// static
uint32_t KeyBinding::AccelModifier() {
switch (WidgetInputEvent::AccelModifier()) {
case MODIFIER_ALT:
return kAlt;
case MODIFIER_CONTROL:
return kControl;
case MODIFIER_META:
return kMeta;
default:
MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()");
return 0;
}
}
void KeyBinding::ToPlatformFormat(nsAString& aValue) const {
nsCOMPtr<nsIStringBundle> keyStringBundle;
nsCOMPtr<nsIStringBundleService> stringBundleService =
mozilla::components::StringBundle::Service();
if (stringBundleService) {
stringBundleService->CreateBundle(
"chrome://global-platform/locale/platformKeys.properties",
getter_AddRefs(keyStringBundle));
}
if (!keyStringBundle) return;
nsAutoString separator;
keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator);
nsAutoString modifierName;
if (mModifierMask & kControl) {
keyStringBundle->GetStringFromName("VK_CONTROL", modifierName);
aValue.Append(modifierName);
aValue.Append(separator);
}
if (mModifierMask & kAlt) {
keyStringBundle->GetStringFromName("VK_ALT", modifierName);
aValue.Append(modifierName);
aValue.Append(separator);
}
if (mModifierMask & kShift) {
keyStringBundle->GetStringFromName("VK_SHIFT", modifierName);
aValue.Append(modifierName);
aValue.Append(separator);
}
if (mModifierMask & kMeta) {
keyStringBundle->GetStringFromName("VK_META", modifierName);
aValue.Append(modifierName);
aValue.Append(separator);
}
aValue.Append(mKey);
}
void KeyBinding::ToAtkFormat(nsAString& aValue) const {
nsAutoString modifierName;
if (mModifierMask & kControl) aValue.AppendLiteral("<Control>");
if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>");
if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>");
if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>");
aValue.Append(mKey);
}