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 "nsMenuItemX.h"
7 #include "nsMenuBarX.h"
9 #include "nsMenuItemIconX.h"
10 #include "nsMenuUtilsX.h"
11 #include "nsCocoaUtils.h"
13 #include "nsObjCExceptions.h"
16 #include "nsGkAtoms.h"
18 #include "mozilla/dom/Element.h"
19 #include "mozilla/dom/Event.h"
20 #include "mozilla/ErrorResult.h"
21 #include "nsIWidget.h"
22 #include "mozilla/dom/Document.h"
24 using namespace mozilla;
26 using mozilla::dom::CallerType;
27 using mozilla::dom::Event;
29 nsMenuItemX::nsMenuItemX(nsMenuX* aParent, const nsString& aLabel,
30 EMenuItemType aItemType,
31 nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
35 mMenuGroupOwner(aMenuGroupOwner) {
36 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
38 MOZ_COUNT_CTOR(nsMenuItemX);
40 MOZ_RELEASE_ASSERT(mContent->IsElement(),
41 "nsMenuItemX should only be created for elements");
42 NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
44 mMenuGroupOwner->RegisterForContentChanges(mContent, this);
46 dom::Document* doc = mContent->GetUncomposedDoc();
48 // if we have a command associated with this menu item, register for changes
49 // to the command DOM node
51 nsAutoString ourCommand;
52 mContent->AsElement()->GetAttr(nsGkAtoms::command, ourCommand);
54 if (!ourCommand.IsEmpty()) {
55 dom::Element* commandElement = doc->GetElementById(ourCommand);
58 mCommandElement = commandElement;
59 // register to observe the command DOM element
60 mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this);
65 // decide enabled state based on command content if it exists, otherwise do it
66 // based on our own content
68 if (mCommandElement) {
69 isEnabled = !mCommandElement->AttrValueIs(
70 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
72 isEnabled = !mContent->AsElement()->AttrValueIs(
73 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
76 // set up the native menu item
77 if (mType == eSeparatorMenuItemType) {
78 mNativeMenuItem = [[NSMenuItem separatorItem] retain];
80 NSString* newCocoaLabelString =
81 nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
82 mNativeMenuItem = [[GeckoNSMenuItem alloc] initWithTitle:newCocoaLabelString
86 mIsChecked = mContent->AsElement()->AttrValueIs(
87 kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters);
89 mNativeMenuItem.enabled = isEnabled;
90 mNativeMenuItem.state =
91 mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
96 mIcon = MakeUnique<nsMenuItemIconX>(this);
98 mIsVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
100 // All menu items other than the "Copy" menu item share the same target and
101 // action, and are differentiated be a unique (representedObject, tag) pair.
102 // The "Copy" menu item is a special case that requires a macOS-default
103 // action of `copy:` and a default target in order for the "Edit" menu to be
104 // populated with OS-provided menu items such as the Emoji picker,
105 // especially in multi-language environments (see bug 1478347). Our
106 // application delegate implements `copy:` by simply forwarding it to
107 // [nsMenuBarX::sNativeEventTarget menuItemHit:].
108 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
109 u"menu_copy"_ns, eCaseMatters)) {
110 mNativeMenuItem.action = @selector(copy:);
112 mNativeMenuItem.action = @selector(menuItemHit:);
113 mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
116 mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
117 mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
123 NS_OBJC_END_TRY_ABORT_BLOCK;
126 nsMenuItemX::~nsMenuItemX() {
127 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
129 // autorelease the native menu item so that anything else happening to this
130 // object happens before the native menu item actually dies
131 [mNativeMenuItem autorelease];
133 DetachFromGroupOwner();
135 MOZ_COUNT_DTOR(nsMenuItemX);
137 NS_OBJC_END_TRY_ABORT_BLOCK;
140 void nsMenuItemX::DetachFromGroupOwner() {
141 if (mMenuGroupOwner) {
142 mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag);
145 mMenuGroupOwner->UnregisterForContentChanges(mContent);
147 if (mCommandElement) {
148 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
152 mMenuGroupOwner = nullptr;
155 nsresult nsMenuItemX::SetChecked(bool aIsChecked) {
156 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
158 mIsChecked = aIsChecked;
160 // update the content model. This will also handle unchecking our siblings
161 // if we are a radiomenu
163 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
166 mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
170 // update native menu item
171 mNativeMenuItem.state =
172 mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
176 NS_OBJC_END_TRY_ABORT_BLOCK;
179 EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; }
181 // Executes the "cached" javaScript command.
182 // Returns NS_OK if the command was executed properly, otherwise an error code.
183 void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags,
185 // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
186 if (mType == eCheckboxMenuItemType ||
187 (mType == eRadioMenuItemType && !mIsChecked)) {
188 if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
189 nsGkAtoms::autocheck,
190 nsGkAtoms::_false, eCaseMatters)) {
191 SetChecked(!mIsChecked);
193 /* the AttributeChanged code will update all the internal state */
196 nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
199 nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
200 bool* preventDefaultCalled) {
202 return NS_ERROR_FAILURE;
205 // get owner document for content
206 nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
210 RefPtr<Event> event =
211 parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
213 NS_WARNING("Failed to create Event");
214 return rv.StealNSResult();
216 event->InitEvent(eventName, true, true);
218 // mark DOM event as trusted
219 event->SetTrusted(true);
222 *preventDefaultCalled =
223 mContent->DispatchEvent(*event, CallerType::System, rv);
225 NS_WARNING("Failed to send DOM event via EventTarget");
226 return rv.StealNSResult();
232 // Walk the sibling list looking for nodes with the same name and
234 void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) {
235 nsAutoString myGroupName;
236 aCheckedContent->AsElement()->GetAttr(nsGkAtoms::name, myGroupName);
237 if (!myGroupName.Length()) { // no groupname, nothing to do
241 nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
246 // loop over siblings
247 for (nsIContent* sibling = parent->GetFirstChild(); sibling;
248 sibling = sibling->GetNextSibling()) {
249 if (sibling != aCheckedContent && sibling->IsElement()) { // skip this node
250 // if the current sibling is in the same group, clear it
251 if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
252 myGroupName, eCaseMatters)) {
253 sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
260 void nsMenuItemX::SetKeyEquiv() {
261 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
263 // Set key shortcut and modifiers
264 nsAutoString keyValue;
265 mContent->AsElement()->GetAttr(nsGkAtoms::key, keyValue);
267 if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) {
268 dom::Element* keyContent =
269 mContent->GetUncomposedDoc()->GetElementById(keyValue);
271 nsAutoString keyChar;
272 bool hasKey = keyContent->GetAttr(nsGkAtoms::key, keyChar);
274 if (!hasKey || keyChar.IsEmpty()) {
275 nsAutoString keyCodeName;
276 keyContent->GetAttr(nsGkAtoms::keycode, keyCodeName);
278 nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
280 keyChar.Assign(charCode);
282 keyChar.AssignLiteral(u" ");
286 nsAutoString modifiersStr;
287 keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
289 nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
291 unsigned int macModifiers =
292 nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
293 mNativeMenuItem.keyEquivalentModifierMask = macModifiers;
295 NSString* keyEquivalent =
296 [[NSString stringWithCharacters:(unichar*)keyChar.get()
297 length:keyChar.Length()] lowercaseString];
298 if ([keyEquivalent isEqualToString:@" "]) {
299 mNativeMenuItem.keyEquivalent = @"";
301 mNativeMenuItem.keyEquivalent = keyEquivalent;
308 // if the key was removed, clear the key
309 mNativeMenuItem.keyEquivalent = @"";
311 NS_OBJC_END_TRY_ABORT_BLOCK;
314 void nsMenuItemX::Dump(uint32_t aIndent) const {
315 printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this,
316 mType == eSeparatorMenuItemType ? "----"
317 : [mNativeMenuItem.title UTF8String],
318 NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
325 void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
326 nsIContent* aContent,
327 nsAtom* aAttribute) {
328 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
334 if (aContent == mContent) { // our own content node changed
335 if (aAttribute == nsGkAtoms::checked) {
336 // if we're a radio menu, uncheck our sibling radio items. No need to
337 // do any of this if we're just a normal check menu.
338 if (mType == eRadioMenuItemType &&
339 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
341 nsGkAtoms::_true, eCaseMatters)) {
342 UncheckRadioSiblings(mContent);
344 mMenuParent->SetRebuild(true);
345 } else if (aAttribute == nsGkAtoms::hidden ||
346 aAttribute == nsGkAtoms::collapsed) {
347 bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
348 if (isVisible != mIsVisible) {
349 mIsVisible = isVisible;
350 RefPtr<nsMenuItemX> self = this;
351 mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self),
357 mMenuParent->SetRebuild(true);
358 } else if (aAttribute == nsGkAtoms::label) {
359 if (mType != eSeparatorMenuItemType) {
360 nsAutoString newLabel;
361 mContent->AsElement()->GetAttr(nsGkAtoms::label, newLabel);
362 mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel);
364 } else if (aAttribute == nsGkAtoms::key) {
366 } else if (aAttribute == nsGkAtoms::image) {
368 } else if (aAttribute == nsGkAtoms::disabled) {
369 mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
370 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
373 } else if (aContent == mCommandElement) {
374 // the only thing that really matters when the menu isn't showing is the
375 // enabled state since it enables/disables keyboard commands
376 if (aAttribute == nsGkAtoms::disabled) {
377 // first we sync our menu item DOM node with the command DOM node
378 nsAutoString commandDisabled;
379 nsAutoString menuDisabled;
380 aContent->AsElement()->GetAttr(nsGkAtoms::disabled, commandDisabled);
381 mContent->AsElement()->GetAttr(nsGkAtoms::disabled, menuDisabled);
382 if (!commandDisabled.Equals(menuDisabled)) {
383 // The menu's disabled state needs to be updated to match the command.
384 if (commandDisabled.IsEmpty()) {
385 mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
386 nsGkAtoms::disabled, true);
388 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
389 commandDisabled, true);
392 // now we sync our native menu item with the command DOM node
393 mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
394 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
399 NS_OBJC_END_TRY_ABORT_BLOCK;
402 bool IsMenuStructureElement(nsIContent* aContent) {
403 return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem,
404 nsGkAtoms::menuseparator);
407 void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument,
408 nsIContent* aContainer,
409 nsIContent* aChild) {
410 MOZ_RELEASE_ASSERT(mMenuGroupOwner);
411 MOZ_RELEASE_ASSERT(mMenuParent);
413 if (aChild == mCommandElement) {
414 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
415 mCommandElement = nullptr;
417 if (IsMenuStructureElement(aChild)) {
418 mMenuParent->SetRebuild(true);
422 void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument,
423 nsIContent* aContainer,
424 nsIContent* aChild) {
425 MOZ_RELEASE_ASSERT(mMenuParent);
427 // The child node could come from the custom element that is for display, so
428 // only rebuild the menu if the child is related to the structure of the
430 if (IsMenuStructureElement(aChild)) {
431 mMenuParent->SetRebuild(true);
435 void nsMenuItemX::SetupIcon() {
436 if (mType != eRegularMenuItemType) {
437 // Don't support icons on checkbox and radio menuitems, for consistency with
442 mIcon->SetupIcon(mContent);
443 mNativeMenuItem.image = mIcon->GetIconImage();
446 void nsMenuItemX::IconUpdated() {
447 mNativeMenuItem.image = mIcon->GetIconImage();