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 "nsAccessibilityService.h"
10 #include "nsAccUtils.h"
11 #include "DocAccessible.h"
12 #include "Role.h"
13 #include "States.h"
14 #include "XULFormControlAccessible.h"
15 
16 #include "nsIContentInlines.h"
17 #include "nsIDOMXULContainerElement.h"
18 #include "nsIDOMXULSelectCntrlEl.h"
19 #include "nsIDOMXULSelectCntrlItemEl.h"
20 #include "nsIContent.h"
21 #include "nsMenuBarFrame.h"
22 #include "nsMenuPopupFrame.h"
23 
24 #include "mozilla/Preferences.h"
25 #include "mozilla/LookAndFeel.h"
26 #include "mozilla/dom/Document.h"
27 #include "mozilla/dom/Element.h"
28 #include "mozilla/dom/KeyboardEventBinding.h"
29 
30 using namespace mozilla;
31 using namespace mozilla::a11y;
32 
33 ////////////////////////////////////////////////////////////////////////////////
34 // XULMenuitemAccessible
35 ////////////////////////////////////////////////////////////////////////////////
36 
XULMenuitemAccessible(nsIContent * aContent,DocAccessible * aDoc)37 XULMenuitemAccessible::XULMenuitemAccessible(nsIContent* aContent,
38                                              DocAccessible* aDoc)
39     : AccessibleWrap(aContent, aDoc) {}
40 
NativeState() const41 uint64_t XULMenuitemAccessible::NativeState() const {
42   uint64_t state = LocalAccessible::NativeState();
43 
44   // Has Popup?
45   if (mContent->NodeInfo()->Equals(nsGkAtoms::menu, kNameSpaceID_XUL)) {
46     state |= states::HASPOPUP;
47     if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::open)) {
48       state |= states::EXPANDED;
49     } else {
50       state |= states::COLLAPSED;
51     }
52   }
53 
54   // Checkable/checked?
55   static dom::Element::AttrValuesArray strings[] = {
56       nsGkAtoms::radio, nsGkAtoms::checkbox, nullptr};
57 
58   if (mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
59                                              strings, eCaseMatters) >= 0) {
60     // Checkable?
61     state |= states::CHECKABLE;
62 
63     // Checked?
64     if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
65                                            nsGkAtoms::checked, nsGkAtoms::_true,
66                                            eCaseMatters)) {
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 
NativeInteractiveState() const108 uint64_t XULMenuitemAccessible::NativeInteractiveState() const {
109   if (NativelyUnavailable()) {
110     // Note: keep in sinc with nsXULPopupManager::IsValidMenuItem() logic.
111     bool skipNavigatingDisabledMenuItem = true;
112     nsMenuFrame* menuFrame = do_QueryFrame(GetFrame());
113     if (!menuFrame || !menuFrame->IsOnMenuBar()) {
114       skipNavigatingDisabledMenuItem =
115           LookAndFeel::GetInt(
116               LookAndFeel::IntID::SkipNavigatingDisabledMenuItem, 0) != 0;
117     }
118 
119     if (skipNavigatingDisabledMenuItem) return states::UNAVAILABLE;
120 
121     return states::UNAVAILABLE | states::FOCUSABLE | states::SELECTABLE;
122   }
123 
124   return states::FOCUSABLE | states::SELECTABLE;
125 }
126 
NativeName(nsString & aName) const127 ENameValueFlag XULMenuitemAccessible::NativeName(nsString& aName) const {
128   mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
129   return eNameOK;
130 }
131 
Description(nsString & aDescription) const132 void XULMenuitemAccessible::Description(nsString& aDescription) const {
133   mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::description,
134                                  aDescription);
135 }
136 
AccessKey() const137 KeyBinding XULMenuitemAccessible::AccessKey() const {
138   // Return menu accesskey: N or Alt+F.
139   static int32_t gMenuAccesskeyModifier =
140       -1;  // magic value of -1 indicates unitialized state
141 
142   // We do not use nsCoreUtils::GetAccesskeyFor() because accesskeys for
143   // menu are't registered by EventStateManager.
144   nsAutoString accesskey;
145   mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
146                                  accesskey);
147   if (accesskey.IsEmpty()) return KeyBinding();
148 
149   uint32_t modifierKey = 0;
150 
151   LocalAccessible* parentAcc = LocalParent();
152   if (parentAcc) {
153     if (parentAcc->NativeRole() == roles::MENUBAR) {
154       // If top level menu item, add Alt+ or whatever modifier text to string
155       // No need to cache pref service, this happens rarely
156       if (gMenuAccesskeyModifier == -1) {
157         // Need to initialize cached global accesskey pref
158         gMenuAccesskeyModifier = Preferences::GetInt("ui.key.menuAccessKey", 0);
159       }
160 
161       switch (gMenuAccesskeyModifier) {
162         case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
163           modifierKey = KeyBinding::kControl;
164           break;
165         case dom::KeyboardEvent_Binding::DOM_VK_ALT:
166           modifierKey = KeyBinding::kAlt;
167           break;
168         case dom::KeyboardEvent_Binding::DOM_VK_META:
169           modifierKey = KeyBinding::kMeta;
170           break;
171         case dom::KeyboardEvent_Binding::DOM_VK_WIN:
172           modifierKey = KeyBinding::kOS;
173           break;
174       }
175     }
176   }
177 
178   return KeyBinding(accesskey[0], modifierKey);
179 }
180 
KeyboardShortcut() const181 KeyBinding XULMenuitemAccessible::KeyboardShortcut() const {
182   nsAutoString keyElmId;
183   mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyElmId);
184   if (keyElmId.IsEmpty()) return KeyBinding();
185 
186   dom::Element* keyElm = mContent->OwnerDoc()->GetElementById(keyElmId);
187   if (!keyElm) return KeyBinding();
188 
189   uint32_t key = 0;
190 
191   nsAutoString keyStr;
192   keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyStr);
193   if (keyStr.IsEmpty()) {
194     nsAutoString keyCodeStr;
195     keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeStr);
196     nsresult errorCode;
197     key = keyStr.ToInteger(&errorCode, /* aRadix = */ 10);
198     if (NS_FAILED(errorCode)) {
199       key = keyStr.ToInteger(&errorCode, /* aRadix = */ 16);
200     }
201   } else {
202     key = keyStr[0];
203   }
204 
205   nsAutoString modifiersStr;
206   keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
207 
208   uint32_t modifierMask = 0;
209   if (modifiersStr.Find("shift") != -1) modifierMask |= KeyBinding::kShift;
210   if (modifiersStr.Find("alt") != -1) modifierMask |= KeyBinding::kAlt;
211   if (modifiersStr.Find("meta") != -1) modifierMask |= KeyBinding::kMeta;
212   if (modifiersStr.Find("os") != -1) modifierMask |= KeyBinding::kOS;
213   if (modifiersStr.Find("control") != -1) modifierMask |= KeyBinding::kControl;
214   if (modifiersStr.Find("accel") != -1) {
215     modifierMask |= KeyBinding::AccelModifier();
216   }
217 
218   return KeyBinding(key, modifierMask);
219 }
220 
NativeRole() const221 role XULMenuitemAccessible::NativeRole() const {
222   nsCOMPtr<nsIDOMXULContainerElement> xulContainer = Elm()->AsXULContainer();
223   if (xulContainer) return roles::PARENT_MENUITEM;
224 
225   LocalAccessible* widget = ContainerWidget();
226   if (widget && widget->Role() == roles::COMBOBOX_LIST) {
227     return roles::COMBOBOX_OPTION;
228   }
229 
230   if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
231                                          nsGkAtoms::radio, eCaseMatters)) {
232     return roles::RADIO_MENU_ITEM;
233   }
234 
235   if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
236                                          nsGkAtoms::checkbox, eCaseMatters)) {
237     return roles::CHECK_MENU_ITEM;
238   }
239 
240   return roles::MENUITEM;
241 }
242 
GetLevel(bool aFast) const243 int32_t XULMenuitemAccessible::GetLevel(bool aFast) const {
244   return nsAccUtils::GetLevelForXULContainerItem(mContent);
245 }
246 
ActionNameAt(uint8_t aIndex,nsAString & aName)247 void XULMenuitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
248   if (aIndex == eAction_Click) aName.AssignLiteral("click");
249 }
250 
HasPrimaryAction() const251 bool XULMenuitemAccessible::HasPrimaryAction() const { return true; }
252 
253 ////////////////////////////////////////////////////////////////////////////////
254 // XULMenuitemAccessible: Widgets
255 
IsActiveWidget() const256 bool XULMenuitemAccessible::IsActiveWidget() const {
257   // Parent menu item is a widget, it's active when its popup is open.
258   // Typically the <menupopup> is included in the document markup, and
259   // <menu> prepends content in front of it.
260   nsIContent* menuPopupContent = mContent->GetLastChild();
261   if (menuPopupContent) {
262     nsMenuPopupFrame* menuPopupFrame =
263         do_QueryFrame(menuPopupContent->GetPrimaryFrame());
264     return menuPopupFrame && menuPopupFrame->IsOpen();
265   }
266   return false;
267 }
268 
AreItemsOperable() const269 bool XULMenuitemAccessible::AreItemsOperable() const {
270   // Parent menu item is a widget, its items are operable when its popup is
271   // open.
272   nsIContent* menuPopupContent = mContent->GetLastChild();
273   if (menuPopupContent) {
274     nsMenuPopupFrame* menuPopupFrame =
275         do_QueryFrame(menuPopupContent->GetPrimaryFrame());
276     return menuPopupFrame && menuPopupFrame->IsOpen();
277   }
278   return false;
279 }
280 
ContainerWidget() const281 LocalAccessible* XULMenuitemAccessible::ContainerWidget() const {
282   nsMenuFrame* menuFrame = do_QueryFrame(GetFrame());
283   if (menuFrame) {
284     nsMenuParent* menuParent = menuFrame->GetMenuParent();
285     if (menuParent) {
286       nsBoxFrame* frame = nullptr;
287       if (menuParent->IsMenuBar()) {  // menubar menu
288         frame = static_cast<nsMenuBarFrame*>(menuParent);
289       } else if (menuParent->IsMenu()) {  // a menupopup or parent menu item
290         frame = static_cast<nsMenuPopupFrame*>(menuParent);
291       }
292       if (frame) {
293         nsIContent* content = frame->GetContent();
294         if (content) {
295           MOZ_ASSERT(mDoc);
296           // We use GetAccessibleOrContainer instead of just GetAccessible
297           // because we strip menupopups from the tree for ATK.
298           return mDoc->GetAccessibleOrContainer(content);
299         }
300       }
301 
302       // otherwise it's different kind of popups (like panel or tooltip), it
303       // shouldn't be a real case.
304     }
305   }
306   return nullptr;
307 }
308 
309 ////////////////////////////////////////////////////////////////////////////////
310 // XULMenuSeparatorAccessible
311 ////////////////////////////////////////////////////////////////////////////////
312 
XULMenuSeparatorAccessible(nsIContent * aContent,DocAccessible * aDoc)313 XULMenuSeparatorAccessible::XULMenuSeparatorAccessible(nsIContent* aContent,
314                                                        DocAccessible* aDoc)
315     : XULMenuitemAccessible(aContent, aDoc) {}
316 
NativeState() const317 uint64_t XULMenuSeparatorAccessible::NativeState() const {
318   // Isn't focusable, but can be offscreen/invisible -- only copy those states
319   return XULMenuitemAccessible::NativeState() &
320          (states::OFFSCREEN | states::INVISIBLE);
321 }
322 
NativeName(nsString & aName) const323 ENameValueFlag XULMenuSeparatorAccessible::NativeName(nsString& aName) const {
324   return eNameOK;
325 }
326 
NativeRole() const327 role XULMenuSeparatorAccessible::NativeRole() const { return roles::SEPARATOR; }
328 
HasPrimaryAction() const329 bool XULMenuSeparatorAccessible::HasPrimaryAction() const { return false; }
330 
331 ////////////////////////////////////////////////////////////////////////////////
332 // XULMenupopupAccessible
333 ////////////////////////////////////////////////////////////////////////////////
334 
XULMenupopupAccessible(nsIContent * aContent,DocAccessible * aDoc)335 XULMenupopupAccessible::XULMenupopupAccessible(nsIContent* aContent,
336                                                DocAccessible* aDoc)
337     : XULSelectControlAccessible(aContent, aDoc) {
338   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
339   if (menuPopupFrame && menuPopupFrame->IsMenu()) mType = eMenuPopupType;
340 
341   // May be the anonymous <menupopup> inside <menulist> (a combobox)
342   auto* parent = mContent->GetParentElement();
343   nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
344       parent ? parent->AsXULSelectControl() : nullptr;
345   if (selectControl) {
346     mSelectControl = parent;
347   } else {
348     mSelectControl = nullptr;
349     mGenericTypes &= ~eSelect;
350   }
351 }
352 
NativeState() const353 uint64_t XULMenupopupAccessible::NativeState() const {
354   uint64_t state = LocalAccessible::NativeState();
355 
356 #ifdef DEBUG
357   // We are onscreen if our parent is active
358   bool isActive =
359       mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::menuactive);
360   if (!isActive) {
361     LocalAccessible* parent = LocalParent();
362     if (parent) {
363       nsIContent* parentContent = parent->GetContent();
364       if (parentContent && parentContent->IsElement())
365         isActive = parentContent->AsElement()->HasAttr(kNameSpaceID_None,
366                                                        nsGkAtoms::open);
367     }
368   }
369 
370   NS_ASSERTION(isActive || (state & states::INVISIBLE),
371                "XULMenupopup doesn't have INVISIBLE when it's inactive");
372 #endif
373 
374   if (state & states::INVISIBLE) state |= states::OFFSCREEN | states::COLLAPSED;
375 
376   return state;
377 }
378 
NativeName(nsString & aName) const379 ENameValueFlag XULMenupopupAccessible::NativeName(nsString& aName) const {
380   nsIContent* content = mContent;
381   while (content && aName.IsEmpty()) {
382     if (content->IsElement()) {
383       content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
384     }
385     content = content->GetFlattenedTreeParent();
386   }
387 
388   return eNameOK;
389 }
390 
NativeRole() const391 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 
IsWidget() const411 bool XULMenupopupAccessible::IsWidget() const { return true; }
412 
IsActiveWidget() const413 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 
AreItemsOperable() const419 bool XULMenupopupAccessible::AreItemsOperable() const {
420   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
421   return menuPopupFrame && menuPopupFrame->IsOpen();
422 }
423 
ContainerWidget() const424 LocalAccessible* XULMenupopupAccessible::ContainerWidget() const {
425   DocAccessible* document = Document();
426 
427   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
428   while (menuPopupFrame) {
429     LocalAccessible* menuPopup =
430         document->GetAccessible(menuPopupFrame->GetContent());
431     if (!menuPopup) {  // shouldn't be a real case
432       return nullptr;
433     }
434 
435     nsMenuFrame* menuFrame = do_QueryFrame(menuPopupFrame->GetParent());
436     if (!menuFrame) {  // context menu or popups
437       return nullptr;
438     }
439 
440     nsMenuParent* menuParent = menuFrame->GetMenuParent();
441     if (!menuParent) {  // menulist or menubutton
442       return menuPopup->LocalParent();
443     }
444 
445     if (menuParent->IsMenuBar()) {  // menubar menu
446       nsMenuBarFrame* menuBarFrame = static_cast<nsMenuBarFrame*>(menuParent);
447       return document->GetAccessible(menuBarFrame->GetContent());
448     }
449 
450     // different kind of popups like panel or tooltip
451     if (!menuParent->IsMenu()) return nullptr;
452 
453     menuPopupFrame = static_cast<nsMenuPopupFrame*>(menuParent);
454   }
455 
456   MOZ_ASSERT_UNREACHABLE("Shouldn't be a real case.");
457   return nullptr;
458 }
459 
460 ////////////////////////////////////////////////////////////////////////////////
461 // XULMenubarAccessible
462 ////////////////////////////////////////////////////////////////////////////////
463 
XULMenubarAccessible(nsIContent * aContent,DocAccessible * aDoc)464 XULMenubarAccessible::XULMenubarAccessible(nsIContent* aContent,
465                                            DocAccessible* aDoc)
466     : AccessibleWrap(aContent, aDoc) {}
467 
NativeName(nsString & aName) const468 ENameValueFlag XULMenubarAccessible::NativeName(nsString& aName) const {
469   aName.AssignLiteral("Application");
470   return eNameOK;
471 }
472 
NativeRole() const473 role XULMenubarAccessible::NativeRole() const { return roles::MENUBAR; }
474 
475 ////////////////////////////////////////////////////////////////////////////////
476 // XULMenubarAccessible: Widgets
477 
IsActiveWidget() const478 bool XULMenubarAccessible::IsActiveWidget() const {
479   nsMenuBarFrame* menuBarFrame = do_QueryFrame(GetFrame());
480   return menuBarFrame && menuBarFrame->IsActive();
481 }
482 
AreItemsOperable() const483 bool XULMenubarAccessible::AreItemsOperable() const { return true; }
484 
CurrentItem() const485 LocalAccessible* XULMenubarAccessible::CurrentItem() const {
486   nsMenuBarFrame* menuBarFrame = do_QueryFrame(GetFrame());
487   if (menuBarFrame) {
488     nsMenuFrame* menuFrame = menuBarFrame->GetCurrentMenuItem();
489     if (menuFrame) {
490       nsIContent* menuItemNode = menuFrame->GetContent();
491       return mDoc->GetAccessible(menuItemNode);
492     }
493   }
494   return nullptr;
495 }
496 
SetCurrentItem(const LocalAccessible * aItem)497 void XULMenubarAccessible::SetCurrentItem(const LocalAccessible* aItem) {
498   NS_ERROR("XULMenubarAccessible::SetCurrentItem not implemented");
499 }
500