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 "Accessible-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 "nsIDOMElement.h"
17 #include "nsIDOMXULElement.h"
18 #include "nsIMutableArray.h"
19 #include "nsIDOMXULContainerElement.h"
20 #include "nsIDOMXULSelectCntrlItemEl.h"
21 #include "nsIDOMXULMultSelectCntrlEl.h"
22 #include "nsIDOMKeyEvent.h"
23 #include "nsIServiceManager.h"
24 #include "nsIPresShell.h"
25 #include "nsIContent.h"
26 #include "nsMenuBarFrame.h"
27 #include "nsMenuPopupFrame.h"
28 
29 #include "mozilla/Preferences.h"
30 #include "mozilla/LookAndFeel.h"
31 #include "mozilla/dom/Element.h"
32 
33 using namespace mozilla;
34 using namespace mozilla::a11y;
35 
36 ////////////////////////////////////////////////////////////////////////////////
37 // XULMenuitemAccessible
38 ////////////////////////////////////////////////////////////////////////////////
39 
40 XULMenuitemAccessible::
XULMenuitemAccessible(nsIContent * aContent,DocAccessible * aDoc)41   XULMenuitemAccessible(nsIContent* aContent, DocAccessible* aDoc) :
42   AccessibleWrap(aContent, aDoc)
43 {
44   mStateFlags |= eNoXBLKids;
45 }
46 
47 uint64_t
NativeState()48 XULMenuitemAccessible::NativeState()
49 {
50   uint64_t state = Accessible::NativeState();
51 
52   // Has Popup?
53   if (mContent->NodeInfo()->Equals(nsGkAtoms::menu, kNameSpaceID_XUL)) {
54     state |= states::HASPOPUP;
55     if (mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::open))
56       state |= states::EXPANDED;
57     else
58       state |= states::COLLAPSED;
59   }
60 
61   // Checkable/checked?
62   static nsIContent::AttrValuesArray strings[] =
63     { &nsGkAtoms::radio, &nsGkAtoms::checkbox, nullptr };
64 
65   if (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, strings,
66                                 eCaseMatters) >= 0) {
67 
68     // Checkable?
69     state |= states::CHECKABLE;
70 
71     // Checked?
72     if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
73                               nsGkAtoms::_true, eCaseMatters))
74       state |= states::CHECKED;
75   }
76 
77   // Combo box listitem
78   bool isComboboxOption = (Role() == roles::COMBOBOX_OPTION);
79   if (isComboboxOption) {
80     // Is selected?
81     bool isSelected = false;
82     nsCOMPtr<nsIDOMXULSelectControlItemElement>
83       item(do_QueryInterface(mContent));
84     NS_ENSURE_TRUE(item, state);
85     item->GetSelected(&isSelected);
86 
87     // Is collapsed?
88     bool isCollapsed = false;
89     Accessible* parent = Parent();
90     if (parent && parent->State() & states::INVISIBLE)
91       isCollapsed = true;
92 
93     if (isSelected) {
94       state |= states::SELECTED;
95 
96       // Selected and collapsed?
97       if (isCollapsed) {
98         // Set selected option offscreen/invisible according to combobox state
99         Accessible* grandParent = parent->Parent();
100         if (!grandParent)
101           return state;
102         NS_ASSERTION(grandParent->Role() == roles::COMBOBOX,
103                      "grandparent of combobox listitem is not combobox");
104         uint64_t grandParentState = grandParent->State();
105         state &= ~(states::OFFSCREEN | states::INVISIBLE);
106         state |= (grandParentState & states::OFFSCREEN) |
107                  (grandParentState & states::INVISIBLE) |
108                  (grandParentState & states::OPAQUE1);
109       } // isCollapsed
110     } // isSelected
111   } // ROLE_COMBOBOX_OPTION
112 
113   return state;
114 }
115 
116 uint64_t
NativeInteractiveState() const117 XULMenuitemAccessible::NativeInteractiveState() const
118 {
119   if (NativelyUnavailable()) {
120     // Note: keep in sinc with nsXULPopupManager::IsValidMenuItem() logic.
121     bool skipNavigatingDisabledMenuItem = true;
122     nsMenuFrame* menuFrame = do_QueryFrame(GetFrame());
123     if (!menuFrame || !menuFrame->IsOnMenuBar()) {
124       skipNavigatingDisabledMenuItem = LookAndFeel::
125         GetInt(LookAndFeel::eIntID_SkipNavigatingDisabledMenuItem, 0) != 0;
126     }
127 
128     if (skipNavigatingDisabledMenuItem)
129       return states::UNAVAILABLE;
130 
131     return states::UNAVAILABLE | states::FOCUSABLE | states::SELECTABLE;
132   }
133 
134   return states::FOCUSABLE | states::SELECTABLE;
135 }
136 
137 ENameValueFlag
NativeName(nsString & aName)138 XULMenuitemAccessible::NativeName(nsString& aName)
139 {
140   mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
141   return eNameOK;
142 }
143 
144 void
Description(nsString & aDescription)145 XULMenuitemAccessible::Description(nsString& aDescription)
146 {
147   mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::description,
148                     aDescription);
149 }
150 
151 KeyBinding
AccessKey() const152 XULMenuitemAccessible::AccessKey() const
153 {
154   // Return menu accesskey: N or Alt+F.
155   static int32_t gMenuAccesskeyModifier = -1;  // magic value of -1 indicates unitialized state
156 
157   // We do not use nsCoreUtils::GetAccesskeyFor() because accesskeys for
158   // menu are't registered by EventStateManager.
159   nsAutoString accesskey;
160   mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
161                     accesskey);
162   if (accesskey.IsEmpty())
163     return KeyBinding();
164 
165   uint32_t modifierKey = 0;
166 
167   Accessible* parentAcc = Parent();
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 nsIDOMKeyEvent::DOM_VK_CONTROL:
179           modifierKey = KeyBinding::kControl;
180           break;
181         case nsIDOMKeyEvent::DOM_VK_ALT:
182           modifierKey = KeyBinding::kAlt;
183           break;
184         case nsIDOMKeyEvent::DOM_VK_META:
185           modifierKey = KeyBinding::kMeta;
186           break;
187         case nsIDOMKeyEvent::DOM_VK_WIN:
188           modifierKey = KeyBinding::kOS;
189           break;
190       }
191     }
192   }
193 
194   return KeyBinding(accesskey[0], modifierKey);
195 }
196 
197 KeyBinding
KeyboardShortcut() const198 XULMenuitemAccessible::KeyboardShortcut() const
199 {
200   nsAutoString keyElmId;
201   mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyElmId);
202   if (keyElmId.IsEmpty())
203     return KeyBinding();
204 
205   nsIContent* keyElm = mContent->OwnerDoc()->GetElementById(keyElmId);
206   if (!keyElm)
207     return KeyBinding();
208 
209   uint32_t key = 0;
210 
211   nsAutoString keyStr;
212   keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyStr);
213   if (keyStr.IsEmpty()) {
214     nsAutoString keyCodeStr;
215     keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeStr);
216     nsresult errorCode;
217     key = keyStr.ToInteger(&errorCode, kAutoDetect);
218   } else {
219     key = keyStr[0];
220   }
221 
222   nsAutoString modifiersStr;
223   keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
224 
225   uint32_t modifierMask = 0;
226   if (modifiersStr.Find("shift") != -1)
227     modifierMask |= KeyBinding::kShift;
228   if (modifiersStr.Find("alt") != -1)
229     modifierMask |= KeyBinding::kAlt;
230   if (modifiersStr.Find("meta") != -1)
231     modifierMask |= KeyBinding::kMeta;
232   if (modifiersStr.Find("os") != -1)
233     modifierMask |= KeyBinding::kOS;
234   if (modifiersStr.Find("control") != -1)
235     modifierMask |= KeyBinding::kControl;
236   if (modifiersStr.Find("accel") != -1) {
237     modifierMask |= KeyBinding::AccelModifier();
238   }
239 
240   return KeyBinding(key, modifierMask);
241 }
242 
243 role
NativeRole()244 XULMenuitemAccessible::NativeRole()
245 {
246   nsCOMPtr<nsIDOMXULContainerElement> xulContainer(do_QueryInterface(mContent));
247   if (xulContainer)
248     return roles::PARENT_MENUITEM;
249 
250   if (mParent && mParent->Role() == roles::COMBOBOX_LIST)
251     return roles::COMBOBOX_OPTION;
252 
253   if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
254                             nsGkAtoms::radio, eCaseMatters))
255     return roles::RADIO_MENU_ITEM;
256 
257   if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
258                             nsGkAtoms::checkbox,
259                             eCaseMatters))
260     return roles::CHECK_MENU_ITEM;
261 
262   return roles::MENUITEM;
263 }
264 
265 int32_t
GetLevelInternal()266 XULMenuitemAccessible::GetLevelInternal()
267 {
268   return nsAccUtils::GetLevelForXULContainerItem(mContent);
269 }
270 
271 bool
DoAction(uint8_t index)272 XULMenuitemAccessible::DoAction(uint8_t index)
273 {
274   if (index == eAction_Click) {   // default action
275     DoCommand();
276     return true;
277   }
278 
279   return false;
280 }
281 
282 void
ActionNameAt(uint8_t aIndex,nsAString & aName)283 XULMenuitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName)
284 {
285   if (aIndex == eAction_Click)
286     aName.AssignLiteral("click");
287 }
288 
289 uint8_t
ActionCount()290 XULMenuitemAccessible::ActionCount()
291 {
292   return 1;
293 }
294 
295 ////////////////////////////////////////////////////////////////////////////////
296 // XULMenuitemAccessible: Widgets
297 
298 bool
IsActiveWidget() const299 XULMenuitemAccessible::IsActiveWidget() const
300 {
301   // Parent menu item is a widget, it's active when its popup is open.
302   nsIContent* menuPopupContent = mContent->GetFirstChild();
303   if (menuPopupContent) {
304     nsMenuPopupFrame* menuPopupFrame =
305       do_QueryFrame(menuPopupContent->GetPrimaryFrame());
306     return menuPopupFrame && menuPopupFrame->IsOpen();
307   }
308   return false;
309 }
310 
311 bool
AreItemsOperable() const312 XULMenuitemAccessible::AreItemsOperable() const
313 {
314   // Parent menu item is a widget, its items are operable when its popup is open.
315   nsIContent* menuPopupContent = mContent->GetFirstChild();
316   if (menuPopupContent) {
317     nsMenuPopupFrame* menuPopupFrame =
318       do_QueryFrame(menuPopupContent->GetPrimaryFrame());
319     return menuPopupFrame && menuPopupFrame->IsOpen();
320   }
321   return false;
322 }
323 
324 Accessible*
ContainerWidget() const325 XULMenuitemAccessible::ContainerWidget() const
326 {
327   nsMenuFrame* menuFrame = do_QueryFrame(GetFrame());
328   if (menuFrame) {
329     nsMenuParent* menuParent = menuFrame->GetMenuParent();
330     if (menuParent) {
331       if (menuParent->IsMenuBar()) // menubar menu
332         return mParent;
333 
334       // a menupoup or parent menu item
335       if (menuParent->IsMenu())
336         return mParent;
337 
338       // otherwise it's different kind of popups (like panel or tooltip), it
339       // shouldn't be a real case.
340     }
341   }
342   return nullptr;
343 }
344 
345 
346 ////////////////////////////////////////////////////////////////////////////////
347 // XULMenuSeparatorAccessible
348 ////////////////////////////////////////////////////////////////////////////////
349 
350 XULMenuSeparatorAccessible::
XULMenuSeparatorAccessible(nsIContent * aContent,DocAccessible * aDoc)351   XULMenuSeparatorAccessible(nsIContent* aContent, DocAccessible* aDoc) :
352   XULMenuitemAccessible(aContent, aDoc)
353 {
354 }
355 
356 uint64_t
NativeState()357 XULMenuSeparatorAccessible::NativeState()
358 {
359   // Isn't focusable, but can be offscreen/invisible -- only copy those states
360   return XULMenuitemAccessible::NativeState() &
361     (states::OFFSCREEN | states::INVISIBLE);
362 }
363 
364 ENameValueFlag
NativeName(nsString & aName)365 XULMenuSeparatorAccessible::NativeName(nsString& aName)
366 {
367   return eNameOK;
368 }
369 
370 role
NativeRole()371 XULMenuSeparatorAccessible::NativeRole()
372 {
373   return roles::SEPARATOR;
374 }
375 
376 bool
DoAction(uint8_t index)377 XULMenuSeparatorAccessible::DoAction(uint8_t index)
378 {
379   return false;
380 }
381 
382 void
ActionNameAt(uint8_t aIndex,nsAString & aName)383 XULMenuSeparatorAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName)
384 {
385   aName.Truncate();
386 }
387 
388 uint8_t
ActionCount()389 XULMenuSeparatorAccessible::ActionCount()
390 {
391   return 0;
392 }
393 
394 ////////////////////////////////////////////////////////////////////////////////
395 // XULMenupopupAccessible
396 ////////////////////////////////////////////////////////////////////////////////
397 
398 XULMenupopupAccessible::
XULMenupopupAccessible(nsIContent * aContent,DocAccessible * aDoc)399   XULMenupopupAccessible(nsIContent* aContent, DocAccessible* aDoc) :
400   XULSelectControlAccessible(aContent, aDoc)
401 {
402   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
403   if (menuPopupFrame && menuPopupFrame->IsMenu())
404     mType = eMenuPopupType;
405 
406   // May be the anonymous <menupopup> inside <menulist> (a combobox)
407   mSelectControl = do_QueryInterface(mContent->GetFlattenedTreeParent());
408   if (!mSelectControl)
409     mGenericTypes &= ~eSelect;
410 
411   mStateFlags |= eNoXBLKids;
412 }
413 
414 uint64_t
NativeState()415 XULMenupopupAccessible::NativeState()
416 {
417   uint64_t state = Accessible::NativeState();
418 
419 #ifdef DEBUG
420   // We are onscreen if our parent is active
421   bool isActive = mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::menuactive);
422   if (!isActive) {
423     Accessible* parent = Parent();
424     if (parent) {
425       nsIContent* parentContent = parent->GetContent();
426       if (parentContent)
427         isActive = parentContent->HasAttr(kNameSpaceID_None, nsGkAtoms::open);
428     }
429   }
430 
431   NS_ASSERTION(isActive || (state & states::INVISIBLE),
432                "XULMenupopup doesn't have INVISIBLE when it's inactive");
433 #endif
434 
435   if (state & states::INVISIBLE)
436     state |= states::OFFSCREEN | states::COLLAPSED;
437 
438   return state;
439 }
440 
441 ENameValueFlag
NativeName(nsString & aName)442 XULMenupopupAccessible::NativeName(nsString& aName)
443 {
444   nsIContent* content = mContent;
445   while (content && aName.IsEmpty()) {
446     content->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
447     content = content->GetFlattenedTreeParent();
448   }
449 
450   return eNameOK;
451 }
452 
453 role
NativeRole()454 XULMenupopupAccessible::NativeRole()
455 {
456   // If accessible is not bound to the tree (this happens while children are
457   // cached) return general role.
458   if (mParent) {
459     roles::Role role = mParent->Role();
460     if (role == roles::COMBOBOX || role == roles::AUTOCOMPLETE)
461       return roles::COMBOBOX_LIST;
462 
463     if (role == roles::PUSHBUTTON) {
464       // Some widgets like the search bar have several popups, owned by buttons.
465       Accessible* grandParent = mParent->Parent();
466       if (grandParent && grandParent->Role() == roles::AUTOCOMPLETE)
467         return roles::COMBOBOX_LIST;
468     }
469   }
470 
471   return roles::MENUPOPUP;
472 }
473 
474 ////////////////////////////////////////////////////////////////////////////////
475 // XULMenupopupAccessible: Widgets
476 
477 bool
IsWidget() const478 XULMenupopupAccessible::IsWidget() const
479 {
480   return true;
481 }
482 
483 bool
IsActiveWidget() const484 XULMenupopupAccessible::IsActiveWidget() const
485 {
486   // If menupopup is a widget (the case of context menus) then active when open.
487   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
488   return menuPopupFrame && menuPopupFrame->IsOpen();
489 }
490 
491 bool
AreItemsOperable() const492 XULMenupopupAccessible::AreItemsOperable() const
493 {
494   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
495   return menuPopupFrame && menuPopupFrame->IsOpen();
496 }
497 
498 Accessible*
ContainerWidget() const499 XULMenupopupAccessible::ContainerWidget() const
500 {
501   DocAccessible* document = Document();
502 
503   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
504   while (menuPopupFrame) {
505     Accessible* menuPopup =
506       document->GetAccessible(menuPopupFrame->GetContent());
507     if (!menuPopup) // shouldn't be a real case
508       return nullptr;
509 
510     nsMenuFrame* menuFrame = do_QueryFrame(menuPopupFrame->GetParent());
511     if (!menuFrame) // context menu or popups
512       return nullptr;
513 
514     nsMenuParent* menuParent = menuFrame->GetMenuParent();
515     if (!menuParent) // menulist or menubutton
516       return menuPopup->Parent();
517 
518     if (menuParent->IsMenuBar()) { // menubar menu
519       nsMenuBarFrame* menuBarFrame = static_cast<nsMenuBarFrame*>(menuParent);
520       return document->GetAccessible(menuBarFrame->GetContent());
521     }
522 
523     // different kind of popups like panel or tooltip
524     if (!menuParent->IsMenu())
525       return nullptr;
526 
527     menuPopupFrame = static_cast<nsMenuPopupFrame*>(menuParent);
528   }
529 
530   NS_NOTREACHED("Shouldn't be a real case.");
531   return nullptr;
532 }
533 
534 ////////////////////////////////////////////////////////////////////////////////
535 // XULMenubarAccessible
536 ////////////////////////////////////////////////////////////////////////////////
537 
538 XULMenubarAccessible::
XULMenubarAccessible(nsIContent * aContent,DocAccessible * aDoc)539   XULMenubarAccessible(nsIContent* aContent, DocAccessible* aDoc) :
540   AccessibleWrap(aContent, aDoc)
541 {
542 }
543 
544 ENameValueFlag
NativeName(nsString & aName)545 XULMenubarAccessible::NativeName(nsString& aName)
546 {
547   aName.AssignLiteral("Application");
548   return eNameOK;
549 }
550 
551 role
NativeRole()552 XULMenubarAccessible::NativeRole()
553 {
554   return roles::MENUBAR;
555 }
556 
557 ////////////////////////////////////////////////////////////////////////////////
558 // XULMenubarAccessible: Widgets
559 
560 bool
IsActiveWidget() const561 XULMenubarAccessible::IsActiveWidget() const
562 {
563   nsMenuBarFrame* menuBarFrame = do_QueryFrame(GetFrame());
564   return menuBarFrame && menuBarFrame->IsActive();
565 }
566 
567 bool
AreItemsOperable() const568 XULMenubarAccessible::AreItemsOperable() const
569 {
570   return true;
571 }
572 
573 Accessible*
CurrentItem()574 XULMenubarAccessible::CurrentItem()
575 {
576   nsMenuBarFrame* menuBarFrame = do_QueryFrame(GetFrame());
577   if (menuBarFrame) {
578     nsMenuFrame* menuFrame = menuBarFrame->GetCurrentMenuItem();
579     if (menuFrame) {
580       nsIContent* menuItemNode = menuFrame->GetContent();
581       return mDoc->GetAccessible(menuItemNode);
582     }
583   }
584   return nullptr;
585 }
586 
587 void
SetCurrentItem(Accessible * aItem)588 XULMenubarAccessible::SetCurrentItem(Accessible* aItem)
589 {
590   NS_ERROR("XULMenubarAccessible::SetCurrentItem not implemented");
591 }
592