no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / widget / cocoa / nsMenuItemX.mm
blob26241217dc25317c24f7e2ac737e73138c37490f
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"
8 #include "nsMenuX.h"
9 #include "nsMenuItemIconX.h"
10 #include "nsMenuUtilsX.h"
11 #include "nsCocoaUtils.h"
13 #include "nsObjCExceptions.h"
15 #include "nsCOMPtr.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)
32     : mContent(aNode),
33       mType(aItemType),
34       mMenuParent(aParent),
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
50   if (doc) {
51     nsAutoString ourCommand;
52     mContent->AsElement()->GetAttr(nsGkAtoms::command, ourCommand);
54     if (!ourCommand.IsEmpty()) {
55       dom::Element* commandElement = doc->GetElementById(ourCommand);
57       if (commandElement) {
58         mCommandElement = commandElement;
59         // register to observe the command DOM element
60         mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this);
61       }
62     }
63   }
65   // decide enabled state based on command content if it exists, otherwise do it
66   // based on our own content
67   bool isEnabled;
68   if (mCommandElement) {
69     isEnabled = !mCommandElement->AttrValueIs(
70         kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
71   } else {
72     isEnabled = !mContent->AsElement()->AttrValueIs(
73         kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
74   }
76   // set up the native menu item
77   if (mType == eSeparatorMenuItemType) {
78     mNativeMenuItem = [[NSMenuItem separatorItem] retain];
79   } else {
80     NSString* newCocoaLabelString =
81         nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
82     mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
83                                                  action:nil
84                                           keyEquivalent:@""];
86     mIsChecked = mContent->AsElement()->AttrValueIs(
87         kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters);
89     mNativeMenuItem.enabled = isEnabled;
90     mNativeMenuItem.state =
91         mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
93     SetKeyEquiv();
94   }
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:);
111   } else {
112     mNativeMenuItem.action = @selector(menuItemHit:);
113     mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
114   }
116   mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
117   mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
119   if (mIsVisible) {
120     SetupIcon();
121   }
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);
144     if (mContent) {
145       mMenuGroupOwner->UnregisterForContentChanges(mContent);
146     }
147     if (mCommandElement) {
148       mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
149     }
150   }
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
162   if (mIsChecked) {
163     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
164                                    u"true"_ns, true);
165   } else {
166     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
167                                      true);
168   }
170   // update native menu item
171   mNativeMenuItem.state =
172       mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
174   return NS_OK;
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,
184                             int16_t aButton) {
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);
192     }
193     /* the AttributeChanged code will update all the internal state */
194   }
196   nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
199 nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
200                                        bool* preventDefaultCalled) {
201   if (!mContent) {
202     return NS_ERROR_FAILURE;
203   }
205   // get owner document for content
206   nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
208   // create DOM event
209   ErrorResult rv;
210   RefPtr<Event> event =
211       parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
212   if (rv.Failed()) {
213     NS_WARNING("Failed to create Event");
214     return rv.StealNSResult();
215   }
216   event->InitEvent(eventName, true, true);
218   // mark DOM event as trusted
219   event->SetTrusted(true);
221   // send DOM event
222   *preventDefaultCalled =
223       mContent->DispatchEvent(*event, CallerType::System, rv);
224   if (rv.Failed()) {
225     NS_WARNING("Failed to send DOM event via EventTarget");
226     return rv.StealNSResult();
227   }
229   return NS_OK;
232 // Walk the sibling list looking for nodes with the same name and
233 // uncheck them all.
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
238     return;
239   }
241   nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
242   if (!parent) {
243     return;
244   }
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,
254                                       u"false"_ns, true);
255       }
256     }
257   }
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);
270     if (keyContent) {
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);
277         uint32_t charCode =
278             nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
279         if (charCode) {
280           keyChar.Assign(charCode);
281         } else {
282           keyChar.AssignLiteral(u" ");
283         }
284       }
286       nsAutoString modifiersStr;
287       keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
288       uint8_t modifiers =
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 = @"";
300       } else {
301         mNativeMenuItem.keyEquivalent = keyEquivalent;
302       }
304       return;
305     }
306   }
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());
322 // nsChangeObserver
325 void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
326                                           nsIContent* aContent,
327                                           nsAtom* aAttribute) {
328   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
330   if (!aContent) {
331     return;
332   }
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,
340                                              nsGkAtoms::checked,
341                                              nsGkAtoms::_true, eCaseMatters)) {
342         UncheckRadioSiblings(mContent);
343       }
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),
352                                                 isVisible);
353         if (mIsVisible) {
354           SetupIcon();
355         }
356       }
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);
363       }
364     } else if (aAttribute == nsGkAtoms::key) {
365       SetKeyEquiv();
366     } else if (aAttribute == nsGkAtoms::image) {
367       SetupIcon();
368     } else if (aAttribute == nsGkAtoms::disabled) {
369       mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
370           kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
371           eCaseMatters);
372     }
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);
387         } else {
388           mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
389                                          commandDisabled, true);
390         }
391       }
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,
395           eCaseMatters);
396     }
397   }
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                                         nsIContent* aPreviousSibling) {
411   MOZ_RELEASE_ASSERT(mMenuGroupOwner);
412   MOZ_RELEASE_ASSERT(mMenuParent);
414   if (aChild == mCommandElement) {
415     mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
416     mCommandElement = nullptr;
417   }
418   if (IsMenuStructureElement(aChild)) {
419     mMenuParent->SetRebuild(true);
420   }
423 void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument,
424                                          nsIContent* aContainer,
425                                          nsIContent* aChild) {
426   MOZ_RELEASE_ASSERT(mMenuParent);
428   // The child node could come from the custom element that is for display, so
429   // only rebuild the menu if the child is related to the structure of the
430   // menu.
431   if (IsMenuStructureElement(aChild)) {
432     mMenuParent->SetRebuild(true);
433   }
436 void nsMenuItemX::SetupIcon() {
437   if (mType != eRegularMenuItemType) {
438     // Don't support icons on checkbox and radio menuitems, for consistency with
439     // Windows & Linux.
440     return;
441   }
443   mIcon->SetupIcon(mContent);
444   mNativeMenuItem.image = mIcon->GetIconImage();
447 void nsMenuItemX::IconUpdated() {
448   mNativeMenuItem.image = mIcon->GetIconImage();