XULMenuAccessible.cpp (16673B)
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/. */ 5 6 #include "XULMenuAccessible.h" 7 8 #include "LocalAccessible-inl.h" 9 #include "XULMenuBarElement.h" 10 #include "XULMenuParentElement.h" 11 #include "XULPopupElement.h" 12 #include "mozilla/Assertions.h" 13 #include "nsAccessibilityService.h" 14 #include "nsAccUtils.h" 15 #include "DocAccessible.h" 16 #include "mozilla/a11y/Role.h" 17 #include "States.h" 18 #include "XULFormControlAccessible.h" 19 20 #include "nsIContentInlines.h" 21 #include "nsIDOMXULContainerElement.h" 22 #include "nsIDOMXULSelectCntrlEl.h" 23 #include "nsIDOMXULSelectCntrlItemEl.h" 24 #include "nsIContent.h" 25 #include "nsMenuPopupFrame.h" 26 27 #include "mozilla/Preferences.h" 28 #include "mozilla/LookAndFeel.h" 29 #include "mozilla/dom/Document.h" 30 #include "mozilla/dom/Element.h" 31 #include "mozilla/dom/XULButtonElement.h" 32 #include "mozilla/dom/KeyboardEventBinding.h" 33 34 using namespace mozilla; 35 using namespace mozilla::a11y; 36 37 //////////////////////////////////////////////////////////////////////////////// 38 // XULMenuitemAccessible 39 //////////////////////////////////////////////////////////////////////////////// 40 41 XULMenuitemAccessible::XULMenuitemAccessible(nsIContent* aContent, 42 DocAccessible* aDoc) 43 : AccessibleWrap(aContent, aDoc) {} 44 45 uint64_t XULMenuitemAccessible::NativeState() const { 46 uint64_t state = LocalAccessible::NativeState(); 47 48 // Has Popup? 49 if (mContent->NodeInfo()->Equals(nsGkAtoms::menu, kNameSpaceID_XUL)) { 50 state |= states::HASPOPUP | states::EXPANDABLE; 51 if (mContent->AsElement()->HasAttr(nsGkAtoms::open)) { 52 state |= states::EXPANDED; 53 } 54 } 55 56 // Checkable/checked? 57 static dom::Element::AttrValuesArray strings[] = { 58 nsGkAtoms::radio, nsGkAtoms::checkbox, nullptr}; 59 60 if (mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, 61 strings, eCaseMatters) >= 0) { 62 // Checkable? 63 state |= states::CHECKABLE; 64 65 // Checked? 66 if (mContent->AsElement()->GetBoolAttr(nsGkAtoms::checked)) { 67 state |= states::CHECKED; 68 } 69 } 70 71 // Combo box listitem 72 bool isComboboxOption = (Role() == roles::COMBOBOX_OPTION); 73 if (isComboboxOption) { 74 // Is selected? 75 bool isSelected = false; 76 nsCOMPtr<nsIDOMXULSelectControlItemElement> item = 77 Elm()->AsXULSelectControlItem(); 78 NS_ENSURE_TRUE(item, state); 79 item->GetSelected(&isSelected); 80 81 // Is collapsed? 82 bool isCollapsed = false; 83 LocalAccessible* parent = LocalParent(); 84 if (parent && parent->State() & states::INVISIBLE) isCollapsed = true; 85 86 if (isSelected) { 87 state |= states::SELECTED; 88 89 // Selected and collapsed? 90 if (isCollapsed) { 91 // Set selected option offscreen/invisible according to combobox state 92 LocalAccessible* grandParent = parent->LocalParent(); 93 if (!grandParent) return state; 94 NS_ASSERTION(grandParent->IsCombobox(), 95 "grandparent of combobox listitem is not combobox"); 96 uint64_t grandParentState = grandParent->State(); 97 state &= ~(states::OFFSCREEN | states::INVISIBLE); 98 state |= (grandParentState & states::OFFSCREEN) | 99 (grandParentState & states::INVISIBLE) | 100 (grandParentState & states::OPAQUE1); 101 } // isCollapsed 102 } // isSelected 103 } // ROLE_COMBOBOX_OPTION 104 105 return state; 106 } 107 108 uint64_t XULMenuitemAccessible::NativeInteractiveState() const { 109 if (NativelyUnavailable()) { 110 // Note: keep in sinc with nsXULPopupManager::IsValidMenuItem() logic. 111 auto* button = dom::XULButtonElement::FromNode(GetContent()); 112 bool skipNavigatingDisabledMenuItem = true; 113 if (!button || !button->IsOnMenuBar()) { 114 skipNavigatingDisabledMenuItem = LookAndFeel::GetInt( 115 LookAndFeel::IntID::SkipNavigatingDisabledMenuItem); 116 } 117 118 if (skipNavigatingDisabledMenuItem) return states::UNAVAILABLE; 119 120 return states::UNAVAILABLE | states::FOCUSABLE | states::SELECTABLE; 121 } 122 123 return states::FOCUSABLE | states::SELECTABLE; 124 } 125 126 ENameValueFlag XULMenuitemAccessible::NativeName(nsString& aName) const { 127 mContent->AsElement()->GetAttr(nsGkAtoms::label, aName); 128 return eNameOK; 129 } 130 131 ENameValueFlag XULMenuitemAccessible::DirectName(nsString& aName) const { 132 ENameValueFlag flag = AccessibleWrap::DirectName(aName); 133 if (!aName.IsEmpty()) { 134 // We can't handle this in NativeName() because some menuitems use 135 // aria-label rather than label, and aria-label is returned by 136 // LocalAccessible::name(). 137 nsAutoString badge; 138 mContent->AsElement()->GetAttr(nsGkAtoms::badge, badge); 139 if (!badge.IsEmpty()) { 140 aName += ' '; 141 aName.Append(badge); 142 } 143 } 144 return flag; 145 } 146 147 EDescriptionValueFlag XULMenuitemAccessible::Description( 148 nsString& aDescription) const { 149 mContent->AsElement()->GetAttr(nsGkAtoms::description, aDescription); 150 151 return eDescriptionOK; 152 } 153 154 KeyBinding XULMenuitemAccessible::AccessKey() const { 155 // Return menu accesskey: N or Alt+F. 156 static int32_t gMenuAccesskeyModifier = 157 -1; // magic value of -1 indicates unitialized state 158 159 // We do not use nsCoreUtils::GetAccesskeyFor() because accesskeys for 160 // menu are't registered by EventStateManager. 161 nsAutoString accesskey; 162 mContent->AsElement()->GetAttr(nsGkAtoms::accesskey, accesskey); 163 if (accesskey.IsEmpty()) return KeyBinding(); 164 165 uint32_t modifierKey = 0; 166 167 LocalAccessible* parentAcc = LocalParent(); 168 if (parentAcc) { 169 if (parentAcc->NativeRole() == roles::MENUBAR) { 170 // If top level menu item, add Alt+ or whatever modifier text to string 171 // No need to cache pref service, this happens rarely 172 if (gMenuAccesskeyModifier == -1) { 173 // Need to initialize cached global accesskey pref 174 gMenuAccesskeyModifier = Preferences::GetInt("ui.key.menuAccessKey", 0); 175 } 176 177 switch (gMenuAccesskeyModifier) { 178 case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: 179 modifierKey = KeyBinding::kControl; 180 break; 181 case dom::KeyboardEvent_Binding::DOM_VK_ALT: 182 modifierKey = KeyBinding::kAlt; 183 break; 184 case dom::KeyboardEvent_Binding::DOM_VK_META: 185 case dom::KeyboardEvent_Binding::DOM_VK_WIN: 186 modifierKey = KeyBinding::kMeta; 187 break; 188 } 189 } 190 } 191 192 return KeyBinding(accesskey[0], modifierKey); 193 } 194 195 KeyBinding XULMenuitemAccessible::KeyboardShortcut() const { 196 nsAutoString keyElmId; 197 mContent->AsElement()->GetAttr(nsGkAtoms::key, keyElmId); 198 if (keyElmId.IsEmpty()) return KeyBinding(); 199 200 dom::Element* keyElm = mContent->OwnerDoc()->GetElementById(keyElmId); 201 if (!keyElm) return KeyBinding(); 202 203 uint32_t key = 0; 204 205 nsAutoString keyStr; 206 keyElm->GetAttr(nsGkAtoms::key, keyStr); 207 if (keyStr.IsEmpty()) { 208 nsAutoString keyCodeStr; 209 keyElm->GetAttr(nsGkAtoms::keycode, keyCodeStr); 210 nsresult errorCode; 211 key = keyStr.ToInteger(&errorCode, /* aRadix = */ 10); 212 if (NS_FAILED(errorCode)) { 213 key = keyStr.ToInteger(&errorCode, /* aRadix = */ 16); 214 } 215 } else { 216 key = keyStr[0]; 217 } 218 219 nsAutoString modifiersStr; 220 keyElm->GetAttr(nsGkAtoms::modifiers, modifiersStr); 221 222 uint32_t modifierMask = 0; 223 if (modifiersStr.Find(u"shift") != -1) modifierMask |= KeyBinding::kShift; 224 if (modifiersStr.Find(u"alt") != -1) modifierMask |= KeyBinding::kAlt; 225 if (modifiersStr.Find(u"meta") != -1) modifierMask |= KeyBinding::kMeta; 226 if (modifiersStr.Find(u"control") != -1) modifierMask |= KeyBinding::kControl; 227 if (modifiersStr.Find(u"accel") != -1) { 228 modifierMask |= KeyBinding::AccelModifier(); 229 } 230 231 return KeyBinding(key, modifierMask); 232 } 233 234 role XULMenuitemAccessible::NativeRole() const { 235 nsCOMPtr<nsIDOMXULContainerElement> xulContainer = Elm()->AsXULContainer(); 236 if (xulContainer) return roles::PARENT_MENUITEM; 237 238 LocalAccessible* widget = ContainerWidget(); 239 if (widget && widget->Role() == roles::COMBOBOX_LIST) { 240 return roles::COMBOBOX_OPTION; 241 } 242 243 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, 244 nsGkAtoms::radio, eCaseMatters)) { 245 return roles::RADIO_MENU_ITEM; 246 } 247 248 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, 249 nsGkAtoms::checkbox, eCaseMatters)) { 250 return roles::CHECK_MENU_ITEM; 251 } 252 253 return roles::MENUITEM; 254 } 255 256 int32_t XULMenuitemAccessible::GetLevel(bool aFast) const { 257 return nsAccUtils::GetLevelForXULContainerItem(mContent); 258 } 259 260 void XULMenuitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { 261 if (aIndex == eAction_Click) aName.AssignLiteral("click"); 262 } 263 264 bool XULMenuitemAccessible::HasPrimaryAction() const { return true; } 265 266 //////////////////////////////////////////////////////////////////////////////// 267 // XULMenuitemAccessible: Widgets 268 269 bool XULMenuitemAccessible::IsActiveWidget() const { 270 // Parent menu item is a widget, it's active when its popup is open. 271 // Typically the <menupopup> is included in the document markup, and 272 // <menu> prepends content in front of it. 273 nsIContent* menuPopupContent = mContent->GetLastChild(); 274 if (menuPopupContent) { 275 nsMenuPopupFrame* menuPopupFrame = 276 do_QueryFrame(menuPopupContent->GetPrimaryFrame()); 277 return menuPopupFrame && menuPopupFrame->IsOpen(); 278 } 279 return false; 280 } 281 282 bool XULMenuitemAccessible::AreItemsOperable() const { 283 // Parent menu item is a widget, its items are operable when its popup is 284 // open. 285 nsIContent* menuPopupContent = mContent->GetLastChild(); 286 if (menuPopupContent) { 287 nsMenuPopupFrame* menuPopupFrame = 288 do_QueryFrame(menuPopupContent->GetPrimaryFrame()); 289 return menuPopupFrame && menuPopupFrame->IsOpen(); 290 } 291 return false; 292 } 293 294 LocalAccessible* XULMenuitemAccessible::ContainerWidget() const { 295 if (auto* button = dom::XULButtonElement::FromNode(GetContent())) { 296 if (auto* popup = button->GetMenuParent()) { 297 // We use GetAccessibleOrContainer instead of just GetAccessible because 298 // we strip menupopups from the tree for ATK. 299 return mDoc->GetAccessibleOrContainer(popup); 300 } 301 } 302 return nullptr; 303 } 304 305 //////////////////////////////////////////////////////////////////////////////// 306 // XULMenuSeparatorAccessible 307 //////////////////////////////////////////////////////////////////////////////// 308 309 XULMenuSeparatorAccessible::XULMenuSeparatorAccessible(nsIContent* aContent, 310 DocAccessible* aDoc) 311 : XULMenuitemAccessible(aContent, aDoc) {} 312 313 uint64_t XULMenuSeparatorAccessible::NativeState() const { 314 // Isn't focusable, but can be offscreen/invisible -- only copy those states 315 return XULMenuitemAccessible::NativeState() & 316 (states::OFFSCREEN | states::INVISIBLE); 317 } 318 319 ENameValueFlag XULMenuSeparatorAccessible::NativeName(nsString& aName) const { 320 return eNameOK; 321 } 322 323 role XULMenuSeparatorAccessible::NativeRole() const { return roles::SEPARATOR; } 324 325 bool XULMenuSeparatorAccessible::HasPrimaryAction() const { return false; } 326 327 //////////////////////////////////////////////////////////////////////////////// 328 // XULMenupopupAccessible 329 //////////////////////////////////////////////////////////////////////////////// 330 331 XULMenupopupAccessible::XULMenupopupAccessible(nsIContent* aContent, 332 DocAccessible* aDoc) 333 : XULSelectControlAccessible(aContent, aDoc) { 334 if (nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame())) { 335 if (menuPopupFrame->GetPopupType() == widget::PopupType::Menu) { 336 mType = eMenuPopupType; 337 } 338 } 339 340 // May be the anonymous <menupopup> inside <menulist> (a combobox) 341 auto* parent = mContent->GetParentElement(); 342 nsCOMPtr<nsIDOMXULSelectControlElement> selectControl = 343 parent ? parent->AsXULSelectControl() : nullptr; 344 if (selectControl) { 345 mSelectControl = parent; 346 } else { 347 mSelectControl = nullptr; 348 mGenericTypes &= ~eSelect; 349 } 350 } 351 352 uint64_t XULMenupopupAccessible::NativeState() const { 353 uint64_t state = LocalAccessible::NativeState(); 354 355 #ifdef DEBUG 356 // We are onscreen if our parent is active 357 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); 358 bool isActive = menuPopupFrame ? menuPopupFrame->IsOpen() : false; 359 if (!isActive) { 360 LocalAccessible* parent = LocalParent(); 361 if (parent) { 362 nsIContent* parentContent = parent->GetContent(); 363 if (parentContent && parentContent->IsElement()) 364 isActive = parentContent->AsElement()->HasAttr(nsGkAtoms::open); 365 } 366 } 367 368 NS_ASSERTION(isActive || (state & states::INVISIBLE), 369 "XULMenupopup doesn't have INVISIBLE when it's inactive"); 370 #endif 371 372 if (state & states::INVISIBLE) { 373 state |= states::OFFSCREEN; 374 } 375 376 return state; 377 } 378 379 ENameValueFlag XULMenupopupAccessible::NativeName(nsString& aName) const { 380 nsIContent* content = mContent; 381 while (content && aName.IsEmpty()) { 382 if (content->IsElement()) { 383 content->AsElement()->GetAttr(nsGkAtoms::label, aName); 384 } 385 content = content->GetFlattenedTreeParent(); 386 } 387 388 return eNameOK; 389 } 390 391 role XULMenupopupAccessible::NativeRole() const { 392 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); 393 if (menuPopupFrame && menuPopupFrame->IsContextMenu()) { 394 return roles::MENUPOPUP; 395 } 396 397 if (mParent) { 398 if (mParent->IsCombobox()) { 399 return roles::COMBOBOX_LIST; 400 } 401 } 402 403 // If accessible is not bound to the tree (this happens while children are 404 // cached) return general role. 405 return roles::MENUPOPUP; 406 } 407 408 //////////////////////////////////////////////////////////////////////////////// 409 // XULMenupopupAccessible: Widgets 410 411 bool XULMenupopupAccessible::IsWidget() const { return true; } 412 413 bool XULMenupopupAccessible::IsActiveWidget() const { 414 // If menupopup is a widget (the case of context menus) then active when open. 415 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); 416 return menuPopupFrame && menuPopupFrame->IsOpen(); 417 } 418 419 bool XULMenupopupAccessible::AreItemsOperable() const { 420 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); 421 return menuPopupFrame && menuPopupFrame->IsOpen(); 422 } 423 424 LocalAccessible* XULMenupopupAccessible::ContainerWidget() const { 425 DocAccessible* document = Document(); 426 427 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); 428 MOZ_ASSERT(menuPopupFrame); 429 if (!menuPopupFrame) { 430 return nullptr; 431 } 432 433 auto* cur = dom::XULPopupElement::FromNode(GetContent()); 434 while (cur) { 435 auto* menu = cur->GetContainingMenu(); 436 if (!menu) { 437 // <panel> / <tooltip> / etc. 438 return nullptr; 439 } 440 dom::XULMenuParentElement* parent = menu->GetMenuParent(); 441 if (!parent) { 442 LocalAccessible* menuPopup = document->GetAccessible(cur); 443 MOZ_ASSERT(menuPopup); 444 return menuPopup ? menuPopup->LocalParent() : nullptr; 445 } 446 447 if (parent->IsMenuBar()) { 448 return document->GetAccessible(parent); 449 } 450 451 cur = dom::XULPopupElement::FromNode(parent); 452 MOZ_ASSERT(cur, "Should be a popup"); 453 } 454 455 MOZ_ASSERT_UNREACHABLE("How did we get out of the loop without returning?"); 456 return nullptr; 457 } 458 459 //////////////////////////////////////////////////////////////////////////////// 460 // XULMenubarAccessible 461 //////////////////////////////////////////////////////////////////////////////// 462 463 XULMenubarAccessible::XULMenubarAccessible(nsIContent* aContent, 464 DocAccessible* aDoc) 465 : AccessibleWrap(aContent, aDoc) {} 466 467 ENameValueFlag XULMenubarAccessible::NativeName(nsString& aName) const { 468 aName.AssignLiteral("Application"); 469 return eNameOK; 470 } 471 472 role XULMenubarAccessible::NativeRole() const { return roles::MENUBAR; } 473 474 //////////////////////////////////////////////////////////////////////////////// 475 // XULMenubarAccessible: Widgets 476 477 bool XULMenubarAccessible::IsActiveWidget() const { 478 auto* menuBar = dom::XULMenuBarElement::FromNode(GetContent()); 479 return menuBar && menuBar->IsActive(); 480 } 481 482 bool XULMenubarAccessible::AreItemsOperable() const { return true; } 483 484 LocalAccessible* XULMenubarAccessible::CurrentItem() const { 485 auto* content = dom::XULMenuParentElement::FromNode(GetContent()); 486 MOZ_ASSERT(content); 487 if (!content || !content->GetActiveMenuChild()) { 488 return nullptr; 489 } 490 return mDoc->GetAccessible(content->GetActiveMenuChild()); 491 } 492 493 void XULMenubarAccessible::SetCurrentItem(const LocalAccessible* aItem) { 494 NS_ERROR("XULMenubarAccessible::SetCurrentItem not implemented"); 495 }