Bug 1845715 - Check for failure when getting RegExp match result template r=iain
[gecko.git] / accessible / basetypes / Accessible.cpp
blobf3b7a6b718b39a172b86ffc0920a03502d7fe665
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "Accessible.h"
7 #include "ARIAMap.h"
8 #include "nsAccUtils.h"
9 #include "nsIURI.h"
10 #include "Relation.h"
11 #include "States.h"
12 #include "mozilla/a11y/FocusManager.h"
13 #include "mozilla/a11y/HyperTextAccessibleBase.h"
14 #include "mozilla/BasicEvents.h"
15 #include "mozilla/Components.h"
16 #include "nsIStringBundle.h"
18 #ifdef A11Y_LOG
19 # include "nsAccessibilityService.h"
20 #endif
22 using namespace mozilla;
23 using namespace mozilla::a11y;
25 Accessible::Accessible()
26 : mType(static_cast<uint32_t>(0)),
27 mGenericTypes(static_cast<uint32_t>(0)),
28 mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {}
30 Accessible::Accessible(AccType aType, AccGenericType aGenericTypes,
31 uint8_t aRoleMapEntryIndex)
32 : mType(static_cast<uint32_t>(aType)),
33 mGenericTypes(static_cast<uint32_t>(aGenericTypes)),
34 mRoleMapEntryIndex(aRoleMapEntryIndex) {}
36 void Accessible::StaticAsserts() const {
37 static_assert(eLastAccType <= (1 << kTypeBits) - 1,
38 "Accessible::mType was oversized by eLastAccType!");
39 static_assert(
40 eLastAccGenericType <= (1 << kGenericTypesBits) - 1,
41 "Accessible::mGenericType was oversized by eLastAccGenericType!");
44 bool Accessible::IsBefore(const Accessible* aAcc) const {
45 // Build the chain of parents.
46 const Accessible* thisP = this;
47 const Accessible* otherP = aAcc;
48 AutoTArray<const Accessible*, 30> thisParents, otherParents;
49 do {
50 thisParents.AppendElement(thisP);
51 thisP = thisP->Parent();
52 } while (thisP);
53 do {
54 otherParents.AppendElement(otherP);
55 otherP = otherP->Parent();
56 } while (otherP);
58 // Find where the parent chain differs.
59 uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length();
60 for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) {
61 const Accessible* thisChild = thisParents.ElementAt(--thisPos);
62 const Accessible* otherChild = otherParents.ElementAt(--otherPos);
63 if (thisChild != otherChild) {
64 return thisChild->IndexInParent() < otherChild->IndexInParent();
68 // If the ancestries are the same length (both thisPos and otherPos are 0),
69 // we should have returned by now.
70 MOZ_ASSERT(thisPos != 0 || otherPos != 0);
71 // At this point, one of the ancestries is a superset of the other, so one of
72 // thisPos or otherPos should be 0.
73 MOZ_ASSERT(thisPos != otherPos);
74 // If the other Accessible is deeper than this one (otherPos > 0), this
75 // Accessible comes before the other.
76 return otherPos > 0;
79 Accessible* Accessible::FocusedChild() {
80 Accessible* doc = nsAccUtils::DocumentFor(this);
81 Accessible* child = doc->FocusedChild();
82 if (child && (child == this || child->Parent() == this)) {
83 return child;
86 return nullptr;
89 const nsRoleMapEntry* Accessible::ARIARoleMap() const {
90 return aria::GetRoleMapFromIndex(mRoleMapEntryIndex);
93 bool Accessible::HasARIARole() const {
94 return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX;
97 bool Accessible::IsARIARole(nsAtom* aARIARole) const {
98 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
99 return roleMapEntry && roleMapEntry->Is(aARIARole);
102 bool Accessible::HasStrongARIARole() const {
103 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
104 return roleMapEntry && roleMapEntry->roleRule == kUseMapRole;
107 bool Accessible::HasGenericType(AccGenericType aType) const {
108 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
109 return (mGenericTypes & aType) ||
110 (roleMapEntry && roleMapEntry->IsOfType(aType));
113 nsIntRect Accessible::BoundsInCSSPixels() const {
114 return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel());
117 LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); }
119 LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) {
120 LayoutDeviceIntPoint point = Bounds().TopLeft();
121 nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
122 this);
123 return point;
126 bool Accessible::IsTextRole() {
127 if (!IsHyperText()) {
128 return false;
131 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
132 if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC ||
133 roleMapEntry->role == roles::IMAGE_MAP ||
134 roleMapEntry->role == roles::SLIDER ||
135 roleMapEntry->role == roles::PROGRESSBAR ||
136 roleMapEntry->role == roles::SEPARATOR)) {
137 return false;
140 return true;
143 uint32_t Accessible::StartOffset() {
144 MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!");
145 Accessible* parent = Parent();
146 HyperTextAccessibleBase* hyperText =
147 parent ? parent->AsHyperTextBase() : nullptr;
148 return hyperText ? hyperText->GetChildOffset(this) : 0;
151 uint32_t Accessible::EndOffset() {
152 MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!");
153 Accessible* parent = Parent();
154 HyperTextAccessibleBase* hyperText =
155 parent ? parent->AsHyperTextBase() : nullptr;
156 return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0;
159 GroupPos Accessible::GroupPosition() {
160 GroupPos groupPos;
162 // Try aria-row/colcount/index.
163 if (IsTableRow()) {
164 Accessible* table = nsAccUtils::TableFor(this);
165 if (table) {
166 if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) {
167 if (*count >= 0) {
168 groupPos.setSize = *count;
172 if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) {
173 groupPos.posInSet = *index;
175 if (groupPos.setSize && groupPos.posInSet) {
176 return groupPos;
179 if (IsTableCell()) {
180 Accessible* table;
181 for (table = Parent(); table; table = table->Parent()) {
182 if (table->IsTable()) {
183 break;
186 if (table) {
187 if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) {
188 if (*count >= 0) {
189 groupPos.setSize = *count;
193 if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) {
194 groupPos.posInSet = *index;
196 if (groupPos.setSize && groupPos.posInSet) {
197 return groupPos;
201 // Get group position from ARIA attributes.
202 ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet);
204 // If ARIA is missed and the accessible is visible then calculate group
205 // position from hierarchy.
206 if (State() & states::INVISIBLE) return groupPos;
208 // Calculate group level if ARIA is missed.
209 if (groupPos.level == 0) {
210 groupPos.level = GetLevel(false);
213 // Calculate position in group and group size if ARIA is missed.
214 if (groupPos.posInSet == 0 || groupPos.setSize == 0) {
215 int32_t posInSet = 0, setSize = 0;
216 GetPositionAndSetSize(&posInSet, &setSize);
217 if (posInSet != 0 && setSize != 0) {
218 if (groupPos.posInSet == 0) groupPos.posInSet = posInSet;
220 if (groupPos.setSize == 0) groupPos.setSize = setSize;
224 return groupPos;
227 int32_t Accessible::GetLevel(bool aFast) const {
228 int32_t level = 0;
229 if (!Parent()) return level;
231 roles::Role role = Role();
232 if (role == roles::OUTLINEITEM) {
233 // Always expose 'level' attribute for 'outlineitem' accessible. The number
234 // of nested 'grouping' accessibles containing 'outlineitem' accessible is
235 // its level.
236 level = 1;
238 if (!aFast) {
239 const Accessible* parent = this;
240 while ((parent = parent->Parent()) && !parent->IsDoc()) {
241 roles::Role parentRole = parent->Role();
243 if (parentRole == roles::OUTLINE) break;
244 if (parentRole == roles::GROUPING) ++level;
247 } else if (role == roles::LISTITEM && !aFast) {
248 // Expose 'level' attribute on nested lists. We support two hierarchies:
249 // a) list -> listitem -> list -> listitem (nested list is a last child
250 // of listitem of the parent list);
251 // b) list -> listitem -> group -> listitem (nested listitems are contained
252 // by group that is a last child of the parent listitem).
254 // Calculate 'level' attribute based on number of parent listitems.
255 level = 0;
256 const Accessible* parent = this;
257 while ((parent = parent->Parent()) && !parent->IsDoc()) {
258 roles::Role parentRole = parent->Role();
260 if (parentRole == roles::LISTITEM) {
261 ++level;
262 } else if (parentRole != roles::LIST && parentRole != roles::GROUPING) {
263 break;
267 if (level == 0) {
268 // If this listitem is on top of nested lists then expose 'level'
269 // attribute.
270 parent = Parent();
271 uint32_t siblingCount = parent->ChildCount();
272 for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) {
273 Accessible* sibling = parent->ChildAt(siblingIdx);
275 Accessible* siblingChild = sibling->LastChild();
276 if (siblingChild) {
277 roles::Role lastChildRole = siblingChild->Role();
278 if (lastChildRole == roles::LIST ||
279 lastChildRole == roles::GROUPING) {
280 return 1;
284 } else {
285 ++level; // level is 1-index based
287 } else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) {
288 if (const Accessible* parent = Parent()) {
289 if (parent->IsHTMLOptGroup()) {
290 return 2;
293 if (parent->IsListControl() && !parent->ARIARoleMap()) {
294 // This is for HTML selects only.
295 if (aFast) {
296 return 1;
299 for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) {
300 if (parent->ChildAt(i)->IsHTMLOptGroup()) {
301 return 1;
306 } else if (role == roles::HEADING) {
307 nsAtom* tagName = TagName();
308 if (tagName == nsGkAtoms::h1) {
309 return 1;
311 if (tagName == nsGkAtoms::h2) {
312 return 2;
314 if (tagName == nsGkAtoms::h3) {
315 return 3;
317 if (tagName == nsGkAtoms::h4) {
318 return 4;
320 if (tagName == nsGkAtoms::h5) {
321 return 5;
323 if (tagName == nsGkAtoms::h6) {
324 return 6;
327 const nsRoleMapEntry* ariaRole = this->ARIARoleMap();
328 if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) {
329 // An aria heading with no aria level has a default level of 2.
330 return 2;
332 } else if (role == roles::COMMENT) {
333 // For comments, count the ancestor elements with the same role to get the
334 // level.
335 level = 1;
337 if (!aFast) {
338 const Accessible* parent = this;
339 while ((parent = parent->Parent()) && !parent->IsDoc()) {
340 roles::Role parentRole = parent->Role();
341 if (parentRole == roles::COMMENT) {
342 ++level;
346 } else if (role == roles::ROW) {
347 // It is a row inside flatten treegrid. Group level is always 1 until it
348 // is overriden by aria-level attribute.
349 const Accessible* parent = Parent();
350 if (parent->Role() == roles::TREE_TABLE) {
351 return 1;
355 return level;
358 void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) {
359 auto groupInfo = GetOrCreateGroupInfo();
360 if (groupInfo) {
361 *aPosInSet = groupInfo->PosInSet();
362 *aSetSize = groupInfo->SetSize();
366 bool Accessible::IsLinkValid() {
367 MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
369 // XXX In order to implement this we would need to follow every link
370 // Perhaps we can get information about invalid links from the cache
371 // In the mean time authors can use role="link" aria-invalid="true"
372 // to force it for links they internally know to be invalid
373 return (0 == (State() & mozilla::a11y::states::INVALID));
376 uint32_t Accessible::AnchorCount() {
377 if (IsImageMap()) {
378 return ChildCount();
381 MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
382 return 1;
385 Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const {
386 if (IsImageMap()) {
387 return ChildAt(aAnchorIndex);
390 MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
391 return aAnchorIndex == 0 ? const_cast<Accessible*>(this) : nullptr;
394 already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const {
395 Accessible* anchor = nullptr;
397 if (IsTextLeaf() || IsImage()) {
398 for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
399 parent = parent->Parent()) {
400 if (parent->IsLink()) {
401 anchor = parent->AnchorAt(aAnchorIndex);
404 } else {
405 anchor = AnchorAt(aAnchorIndex);
408 if (anchor) {
409 RefPtr<nsIURI> uri;
410 nsAutoString spec;
411 anchor->Value(spec);
412 nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
413 if (NS_SUCCEEDED(rv)) {
414 return uri.forget();
418 return nullptr;
421 bool Accessible::IsSearchbox() const {
422 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
423 if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) {
424 return true;
427 RefPtr<nsAtom> inputType = InputType();
428 return inputType == nsGkAtoms::search;
431 #ifdef A11Y_LOG
432 void Accessible::DebugDescription(nsCString& aDesc) const {
433 aDesc.Truncate();
434 aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local");
435 aDesc.AppendPrintf("[%p] ", this);
436 nsAutoString role;
437 GetAccService()->GetStringRole(Role(), role);
438 aDesc.Append(NS_ConvertUTF16toUTF8(role));
440 if (nsAtom* tagAtom = TagName()) {
441 nsAutoCString tag;
442 tagAtom->ToUTF8String(tag);
443 aDesc.AppendPrintf(" %s", tag.get());
445 nsAutoString id;
446 DOMNodeID(id);
447 if (!id.IsEmpty()) {
448 aDesc.Append("#");
449 aDesc.Append(NS_ConvertUTF16toUTF8(id));
452 nsAutoString id;
454 nsAutoString name;
455 Name(name);
456 if (!name.IsEmpty()) {
457 aDesc.Append(" '");
458 aDesc.Append(NS_ConvertUTF16toUTF8(name));
459 aDesc.Append("'");
463 void Accessible::DebugPrint(const char* aPrefix,
464 const Accessible* aAccessible) {
465 nsAutoCString desc;
466 if (aAccessible) {
467 aAccessible->DebugDescription(desc);
468 } else {
469 desc.AssignLiteral("[null]");
471 # if defined(ANDROID)
472 printf_stderr("%s %s\n", aPrefix, desc.get());
473 # else
474 printf("%s %s\n", aPrefix, desc.get());
475 # endif
478 #endif
480 void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut) {
481 nsCOMPtr<nsIStringBundleService> stringBundleService =
482 components::StringBundle::Service();
483 if (!stringBundleService) return;
485 nsCOMPtr<nsIStringBundle> stringBundle;
486 stringBundleService->CreateBundle(
487 "chrome://global-platform/locale/accessible.properties",
488 getter_AddRefs(stringBundle));
489 if (!stringBundle) return;
491 nsAutoString xsValue;
492 nsresult rv = stringBundle->GetStringFromName(
493 NS_ConvertUTF16toUTF8(aKey).get(), xsValue);
494 if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue);
497 const Accessible* Accessible::ActionAncestor() const {
498 // We do want to consider a click handler on the document. However, we don't
499 // want to walk outside of this document, so we stop if we see an OuterDoc.
500 for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
501 parent = parent->Parent()) {
502 if (parent->HasPrimaryAction()) {
503 return parent;
507 return nullptr;
510 nsStaticAtom* Accessible::LandmarkRole() const {
511 nsAtom* tagName = TagName();
512 if (!tagName) {
513 // Either no associated content, or no cache.
514 return nullptr;
517 if (tagName == nsGkAtoms::nav) {
518 return nsGkAtoms::navigation;
521 if (tagName == nsGkAtoms::aside) {
522 return nsGkAtoms::complementary;
525 if (tagName == nsGkAtoms::main) {
526 return nsGkAtoms::main;
529 if (tagName == nsGkAtoms::header) {
530 if (Role() == roles::LANDMARK) {
531 return nsGkAtoms::banner;
535 if (tagName == nsGkAtoms::footer) {
536 if (Role() == roles::LANDMARK) {
537 return nsGkAtoms::contentinfo;
541 if (tagName == nsGkAtoms::section) {
542 nsAutoString name;
543 Name(name);
544 if (!name.IsEmpty()) {
545 return nsGkAtoms::region;
549 if (tagName == nsGkAtoms::form) {
550 nsAutoString name;
551 Name(name);
552 if (!name.IsEmpty()) {
553 return nsGkAtoms::form;
557 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
558 return roleMapEntry && roleMapEntry->IsOfType(eLandmark)
559 ? roleMapEntry->roleAtom
560 : nullptr;
563 nsStaticAtom* Accessible::ComputedARIARole() const {
564 const nsRoleMapEntry* roleMap = ARIARoleMap();
565 if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty &&
566 // region has its own Gecko role and it needs to be handled specially.
567 roleMap->roleAtom != nsGkAtoms::region &&
568 (roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) ||
569 roleMap->roleAtom == nsGkAtoms::alertdialog ||
570 roleMap->roleAtom == nsGkAtoms::feed ||
571 roleMap->roleAtom == nsGkAtoms::rowgroup ||
572 roleMap->roleAtom == nsGkAtoms::searchbox)) {
573 // Explicit ARIA role (e.g. specified via the role attribute) which does not
574 // map to a unique Gecko role.
575 return roleMap->roleAtom;
577 role geckoRole = Role();
578 if (geckoRole == roles::LANDMARK) {
579 // Landmark role from native markup; e.g. <main>, <nav>.
580 return LandmarkRole();
582 if (geckoRole == roles::GROUPING) {
583 // Gecko doesn't differentiate between group and rowgroup. It uses
584 // roles::GROUPING for both.
585 nsAtom* tag = TagName();
586 if (tag == nsGkAtoms::tbody || tag == nsGkAtoms::tfoot ||
587 tag == nsGkAtoms::thead) {
588 return nsGkAtoms::rowgroup;
591 // Role from native markup or layout.
592 #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
593 msaaRole, ia2Role, androidClass, nameRule) \
594 case roles::_geckoRole: \
595 return ariaRole;
596 switch (geckoRole) {
597 #include "RoleMap.h"
599 #undef ROLE
600 MOZ_ASSERT_UNREACHABLE("Unknown role");
601 return nullptr;
604 void Accessible::ApplyImplicitState(uint64_t& aState) const {
605 // nsAccessibilityService (and thus FocusManager) can be shut down before
606 // RemoteAccessibles.
607 if (const auto* focusMgr = FocusMgr()) {
608 if (focusMgr->IsFocused(this)) {
609 aState |= states::FOCUSED;
613 // If this is an ARIA item of the selectable widget and if it's focused and
614 // not marked unselected explicitly (i.e. aria-selected="false") then expose
615 // it as selected to make ARIA widget authors life easier.
616 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
617 if (roleMapEntry && !(aState & states::SELECTED) &&
618 ARIASelected().valueOr(true)) {
619 // Special case for tabs: focused tab or focus inside related tab panel
620 // implies selected state.
621 if (roleMapEntry->role == roles::PAGETAB) {
622 if (aState & states::FOCUSED) {
623 aState |= states::SELECTED;
624 } else {
625 // If focus is in a child of the tab panel surely the tab is selected!
626 Relation rel = RelationByType(RelationType::LABEL_FOR);
627 Accessible* relTarget = nullptr;
628 while ((relTarget = rel.Next())) {
629 if (relTarget->Role() == roles::PROPERTYPAGE &&
630 FocusMgr()->IsFocusWithin(relTarget)) {
631 aState |= states::SELECTED;
635 } else if (aState & states::FOCUSED) {
636 Accessible* container = nsAccUtils::GetSelectableContainer(this, aState);
637 if (container && !(container->State() & states::MULTISELECTABLE)) {
638 aState |= states::SELECTED;
643 if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) {
644 aState |= states::OPAQUE1;
648 ////////////////////////////////////////////////////////////////////////////////
649 // KeyBinding class
651 // static
652 uint32_t KeyBinding::AccelModifier() {
653 switch (WidgetInputEvent::AccelModifier()) {
654 case MODIFIER_ALT:
655 return kAlt;
656 case MODIFIER_CONTROL:
657 return kControl;
658 case MODIFIER_META:
659 return kMeta;
660 case MODIFIER_OS:
661 return kOS;
662 default:
663 MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()");
664 return 0;
668 void KeyBinding::ToPlatformFormat(nsAString& aValue) const {
669 nsCOMPtr<nsIStringBundle> keyStringBundle;
670 nsCOMPtr<nsIStringBundleService> stringBundleService =
671 mozilla::components::StringBundle::Service();
672 if (stringBundleService) {
673 stringBundleService->CreateBundle(
674 "chrome://global-platform/locale/platformKeys.properties",
675 getter_AddRefs(keyStringBundle));
678 if (!keyStringBundle) return;
680 nsAutoString separator;
681 keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator);
683 nsAutoString modifierName;
684 if (mModifierMask & kControl) {
685 keyStringBundle->GetStringFromName("VK_CONTROL", modifierName);
687 aValue.Append(modifierName);
688 aValue.Append(separator);
691 if (mModifierMask & kAlt) {
692 keyStringBundle->GetStringFromName("VK_ALT", modifierName);
694 aValue.Append(modifierName);
695 aValue.Append(separator);
698 if (mModifierMask & kShift) {
699 keyStringBundle->GetStringFromName("VK_SHIFT", modifierName);
701 aValue.Append(modifierName);
702 aValue.Append(separator);
705 if (mModifierMask & kMeta) {
706 keyStringBundle->GetStringFromName("VK_META", modifierName);
708 aValue.Append(modifierName);
709 aValue.Append(separator);
712 aValue.Append(mKey);
715 void KeyBinding::ToAtkFormat(nsAString& aValue) const {
716 nsAutoString modifierName;
717 if (mModifierMask & kControl) aValue.AppendLiteral("<Control>");
719 if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>");
721 if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>");
723 if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>");
725 aValue.Append(mKey);