no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / widget / cocoa / nsMenuItemX.mm
bloba2d511c8d2b10453e2709a04c771b6e2ef83cc23
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 share the same target and action, and are differentiated
101   // be a unique (representedObject, tag) pair.
102   mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
103   mNativeMenuItem.action = @selector(menuItemHit:);
104   mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
105   mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
107   if (mIsVisible) {
108     SetupIcon();
109   }
111   NS_OBJC_END_TRY_ABORT_BLOCK;
114 nsMenuItemX::~nsMenuItemX() {
115   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
117   // autorelease the native menu item so that anything else happening to this
118   // object happens before the native menu item actually dies
119   [mNativeMenuItem autorelease];
121   DetachFromGroupOwner();
123   MOZ_COUNT_DTOR(nsMenuItemX);
125   NS_OBJC_END_TRY_ABORT_BLOCK;
128 void nsMenuItemX::DetachFromGroupOwner() {
129   if (mMenuGroupOwner) {
130     mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag);
132     if (mContent) {
133       mMenuGroupOwner->UnregisterForContentChanges(mContent);
134     }
135     if (mCommandElement) {
136       mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
137     }
138   }
140   mMenuGroupOwner = nullptr;
143 nsresult nsMenuItemX::SetChecked(bool aIsChecked) {
144   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
146   mIsChecked = aIsChecked;
148   // update the content model. This will also handle unchecking our siblings
149   // if we are a radiomenu
150   if (mIsChecked) {
151     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
152                                    u"true"_ns, true);
153   } else {
154     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
155                                      true);
156   }
158   // update native menu item
159   mNativeMenuItem.state =
160       mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
162   return NS_OK;
164   NS_OBJC_END_TRY_ABORT_BLOCK;
167 EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; }
169 // Executes the "cached" javaScript command.
170 // Returns NS_OK if the command was executed properly, otherwise an error code.
171 void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags,
172                             int16_t aButton) {
173   // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
174   if (mType == eCheckboxMenuItemType ||
175       (mType == eRadioMenuItemType && !mIsChecked)) {
176     if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
177                                             nsGkAtoms::autocheck,
178                                             nsGkAtoms::_false, eCaseMatters)) {
179       SetChecked(!mIsChecked);
180     }
181     /* the AttributeChanged code will update all the internal state */
182   }
184   nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
187 nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
188                                        bool* preventDefaultCalled) {
189   if (!mContent) {
190     return NS_ERROR_FAILURE;
191   }
193   // get owner document for content
194   nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
196   // create DOM event
197   ErrorResult rv;
198   RefPtr<Event> event =
199       parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
200   if (rv.Failed()) {
201     NS_WARNING("Failed to create Event");
202     return rv.StealNSResult();
203   }
204   event->InitEvent(eventName, true, true);
206   // mark DOM event as trusted
207   event->SetTrusted(true);
209   // send DOM event
210   *preventDefaultCalled =
211       mContent->DispatchEvent(*event, CallerType::System, rv);
212   if (rv.Failed()) {
213     NS_WARNING("Failed to send DOM event via EventTarget");
214     return rv.StealNSResult();
215   }
217   return NS_OK;
220 // Walk the sibling list looking for nodes with the same name and
221 // uncheck them all.
222 void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) {
223   nsAutoString myGroupName;
224   aCheckedContent->AsElement()->GetAttr(nsGkAtoms::name, myGroupName);
225   if (!myGroupName.Length()) {  // no groupname, nothing to do
226     return;
227   }
229   nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
230   if (!parent) {
231     return;
232   }
234   // loop over siblings
235   for (nsIContent* sibling = parent->GetFirstChild(); sibling;
236        sibling = sibling->GetNextSibling()) {
237     if (sibling != aCheckedContent && sibling->IsElement()) {  // skip this node
238       // if the current sibling is in the same group, clear it
239       if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
240                                             myGroupName, eCaseMatters)) {
241         sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
242                                       u"false"_ns, true);
243       }
244     }
245   }
248 void nsMenuItemX::SetKeyEquiv() {
249   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
251   // Set key shortcut and modifiers
252   nsAutoString keyValue;
253   mContent->AsElement()->GetAttr(nsGkAtoms::key, keyValue);
255   if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) {
256     dom::Element* keyContent =
257         mContent->GetUncomposedDoc()->GetElementById(keyValue);
258     if (keyContent) {
259       nsAutoString keyChar;
260       bool hasKey = keyContent->GetAttr(nsGkAtoms::key, keyChar);
262       if (!hasKey || keyChar.IsEmpty()) {
263         nsAutoString keyCodeName;
264         keyContent->GetAttr(nsGkAtoms::keycode, keyCodeName);
265         uint32_t charCode =
266             nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
267         if (charCode) {
268           keyChar.Assign(charCode);
269         } else {
270           keyChar.AssignLiteral(u" ");
271         }
272       }
274       nsAutoString modifiersStr;
275       keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
276       uint8_t modifiers =
277           nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
279       unsigned int macModifiers =
280           nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
281       mNativeMenuItem.keyEquivalentModifierMask = macModifiers;
283       NSString* keyEquivalent =
284           [[NSString stringWithCharacters:(unichar*)keyChar.get()
285                                    length:keyChar.Length()] lowercaseString];
286       if ([keyEquivalent isEqualToString:@" "]) {
287         mNativeMenuItem.keyEquivalent = @"";
288       } else {
289         mNativeMenuItem.keyEquivalent = keyEquivalent;
290       }
292       return;
293     }
294   }
296   // if the key was removed, clear the key
297   mNativeMenuItem.keyEquivalent = @"";
299   NS_OBJC_END_TRY_ABORT_BLOCK;
302 void nsMenuItemX::Dump(uint32_t aIndent) const {
303   printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this,
304          mType == eSeparatorMenuItemType ? "----"
305                                          : [mNativeMenuItem.title UTF8String],
306          NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
310 // nsChangeObserver
313 void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
314                                           nsIContent* aContent,
315                                           nsAtom* aAttribute) {
316   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
318   if (!aContent) {
319     return;
320   }
322   if (aContent == mContent) {  // our own content node changed
323     if (aAttribute == nsGkAtoms::checked) {
324       // if we're a radio menu, uncheck our sibling radio items. No need to
325       // do any of this if we're just a normal check menu.
326       if (mType == eRadioMenuItemType &&
327           mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
328                                              nsGkAtoms::checked,
329                                              nsGkAtoms::_true, eCaseMatters)) {
330         UncheckRadioSiblings(mContent);
331       }
332       mMenuParent->SetRebuild(true);
333     } else if (aAttribute == nsGkAtoms::hidden ||
334                aAttribute == nsGkAtoms::collapsed) {
335       bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
336       if (isVisible != mIsVisible) {
337         mIsVisible = isVisible;
338         RefPtr<nsMenuItemX> self = this;
339         mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self),
340                                                 isVisible);
341         if (mIsVisible) {
342           SetupIcon();
343         }
344       }
345       mMenuParent->SetRebuild(true);
346     } else if (aAttribute == nsGkAtoms::label) {
347       if (mType != eSeparatorMenuItemType) {
348         nsAutoString newLabel;
349         mContent->AsElement()->GetAttr(nsGkAtoms::label, newLabel);
350         mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel);
351       }
352     } else if (aAttribute == nsGkAtoms::key) {
353       SetKeyEquiv();
354     } else if (aAttribute == nsGkAtoms::image) {
355       SetupIcon();
356     } else if (aAttribute == nsGkAtoms::disabled) {
357       mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
358           kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
359           eCaseMatters);
360     }
361   } else if (aContent == mCommandElement) {
362     // the only thing that really matters when the menu isn't showing is the
363     // enabled state since it enables/disables keyboard commands
364     if (aAttribute == nsGkAtoms::disabled) {
365       // first we sync our menu item DOM node with the command DOM node
366       nsAutoString commandDisabled;
367       nsAutoString menuDisabled;
368       aContent->AsElement()->GetAttr(nsGkAtoms::disabled, commandDisabled);
369       mContent->AsElement()->GetAttr(nsGkAtoms::disabled, menuDisabled);
370       if (!commandDisabled.Equals(menuDisabled)) {
371         // The menu's disabled state needs to be updated to match the command.
372         if (commandDisabled.IsEmpty()) {
373           mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
374                                            nsGkAtoms::disabled, true);
375         } else {
376           mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
377                                          commandDisabled, true);
378         }
379       }
380       // now we sync our native menu item with the command DOM node
381       mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
382           kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
383           eCaseMatters);
384     }
385   }
387   NS_OBJC_END_TRY_ABORT_BLOCK;
390 bool IsMenuStructureElement(nsIContent* aContent) {
391   return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem,
392                                       nsGkAtoms::menuseparator);
395 void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument,
396                                         nsIContent* aContainer,
397                                         nsIContent* aChild,
398                                         nsIContent* aPreviousSibling) {
399   MOZ_RELEASE_ASSERT(mMenuGroupOwner);
400   MOZ_RELEASE_ASSERT(mMenuParent);
402   if (aChild == mCommandElement) {
403     mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
404     mCommandElement = nullptr;
405   }
406   if (IsMenuStructureElement(aChild)) {
407     mMenuParent->SetRebuild(true);
408   }
411 void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument,
412                                          nsIContent* aContainer,
413                                          nsIContent* aChild) {
414   MOZ_RELEASE_ASSERT(mMenuParent);
416   // The child node could come from the custom element that is for display, so
417   // only rebuild the menu if the child is related to the structure of the
418   // menu.
419   if (IsMenuStructureElement(aChild)) {
420     mMenuParent->SetRebuild(true);
421   }
424 void nsMenuItemX::SetupIcon() {
425   if (mType != eRegularMenuItemType) {
426     // Don't support icons on checkbox and radio menuitems, for consistency with
427     // Windows & Linux.
428     return;
429   }
431   mIcon->SetupIcon(mContent);
432   mNativeMenuItem.image = mIcon->GetIconImage();
435 void nsMenuItemX::IconUpdated() {
436   mNativeMenuItem.image = mIcon->GetIconImage();