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