Bumping manifests a=b2g-bump
[gecko.git] / widget / cocoa / nsMenuItemX.mm
blob5c38c6f45cb399c0f792e8876c0bce48fdae8c0b
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 "nsIWidget.h"
20 #include "nsIDocument.h"
21 #include "nsIDOMDocument.h"
22 #include "nsIDOMElement.h"
23 #include "nsIDOMEvent.h"
25 nsMenuItemX::nsMenuItemX()
27   mType           = eRegularMenuItemType;
28   mNativeMenuItem = nil;
29   mMenuParent     = nullptr;
30   mMenuGroupOwner = nullptr;
31   mIsChecked      = false;
33   MOZ_COUNT_CTOR(nsMenuItemX);
36 nsMenuItemX::~nsMenuItemX()
38   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
40   // Prevent the icon object from outliving us.
41   if (mIcon)
42     mIcon->Destroy();
44   // autorelease the native menu item so that anything else happening to this
45   // object happens before the native menu item actually dies
46   [mNativeMenuItem autorelease];
48   if (mContent)
49     mMenuGroupOwner->UnregisterForContentChanges(mContent);
50   if (mCommandContent)
51     mMenuGroupOwner->UnregisterForContentChanges(mCommandContent);
53   MOZ_COUNT_DTOR(nsMenuItemX);
55   NS_OBJC_END_TRY_ABORT_BLOCK;
58 nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType,
59                              nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
61   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
63   mType = aItemType;
64   mMenuParent = aParent;
65   mContent = aNode;
67   mMenuGroupOwner = aMenuGroupOwner;
68   NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
70   mMenuGroupOwner->RegisterForContentChanges(mContent, this);
72   nsIDocument *doc = mContent->GetCurrentDoc();
74   // if we have a command associated with this menu item, register for changes
75   // to the command DOM node
76   if (doc) {
77     nsAutoString ourCommand;
78     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand);
80     if (!ourCommand.IsEmpty()) {
81       nsIContent *commandElement = doc->GetElementById(ourCommand);
83       if (commandElement) {
84         mCommandContent = commandElement;
85         // register to observe the command DOM element
86         mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this);
87       }
88     }
89   }
91   // decide enabled state based on command content if it exists, otherwise do it based
92   // on our own content
93   bool isEnabled;
94   if (mCommandContent)
95     isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
96   else
97     isEnabled = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
99   // set up the native menu item
100   if (mType == eSeparatorMenuItemType) {
101     mNativeMenuItem = [[NSMenuItem separatorItem] retain];
102   }
103   else {
104     NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
105     mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""];
107     [mNativeMenuItem setEnabled:(BOOL)isEnabled];
109     SetChecked(mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
110                                      nsGkAtoms::_true, eCaseMatters));
111     SetKeyEquiv();
112   }
114   mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem);
115   if (!mIcon)
116     return NS_ERROR_OUT_OF_MEMORY;
118   return NS_OK;
120   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
123 nsresult nsMenuItemX::SetChecked(bool aIsChecked)
125   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
127   mIsChecked = aIsChecked;
128   
129   // update the content model. This will also handle unchecking our siblings
130   // if we are a radiomenu
131   mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, 
132                     mIsChecked ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false"), true);
133   
134   // update native menu item
135   if (mIsChecked)
136     [mNativeMenuItem setState:NSOnState];
137   else
138     [mNativeMenuItem setState:NSOffState];
140   return NS_OK;
142   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
145 EMenuItemType nsMenuItemX::GetMenuItemType()
147   return mType;
150 // Executes the "cached" javaScript command.
151 // Returns NS_OK if the command was executed properly, otherwise an error code.
152 void nsMenuItemX::DoCommand()
154   // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
155   if (mType == eCheckboxMenuItemType ||
156       (mType == eRadioMenuItemType && !mIsChecked)) {
157     if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
158                                nsGkAtoms::_false, eCaseMatters))
159     SetChecked(!mIsChecked);
160     /* the AttributeChanged code will update all the internal state */
161   }
163   nsMenuUtilsX::DispatchCommandTo(mContent);
166 nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled)
168   if (!mContent)
169     return NS_ERROR_FAILURE;
171   // get owner document for content
172   nsCOMPtr<nsIDocument> parentDoc = mContent->OwnerDoc();
174   // get interface for creating DOM events from content owner document
175   nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(parentDoc);
176   if (!domDoc) {
177     NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument");
178     return NS_ERROR_FAILURE;
179   }
181   // create DOM event
182   nsCOMPtr<nsIDOMEvent> event;
183   nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event));
184   if (NS_FAILED(rv)) {
185     NS_WARNING("Failed to create nsIDOMEvent");
186     return rv;
187   }
188   event->InitEvent(eventName, true, true);
190   // mark DOM event as trusted
191   event->SetTrusted(true);
193   // send DOM event
194   rv = mContent->DispatchEvent(event, preventDefaultCalled);
195   if (NS_FAILED(rv)) {
196     NS_WARNING("Failed to send DOM event via EventTarget");
197     return rv;
198   }
200   return NS_OK;
203 // Walk the sibling list looking for nodes with the same name and
204 // uncheck them all.
205 void nsMenuItemX::UncheckRadioSiblings(nsIContent* inCheckedContent)
207   nsAutoString myGroupName;
208   inCheckedContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName);
209   if (!myGroupName.Length()) // no groupname, nothing to do
210     return;
212   nsCOMPtr<nsIContent> parent = inCheckedContent->GetParent();
213   if (!parent)
214     return;
216   // loop over siblings
217   uint32_t count = parent->GetChildCount();
218   for (uint32_t i = 0; i < count; i++) {
219     nsIContent *sibling = parent->GetChildAt(i);
220     if (sibling) {      
221       if (sibling != inCheckedContent) { // skip this node
222         // if the current sibling is in the same group, clear it
223         if (sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
224                                  myGroupName, eCaseMatters))
225           sibling->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("false"), true);
226       }
227     }    
228   }
231 void nsMenuItemX::SetKeyEquiv()
233   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
235   // Set key shortcut and modifiers
236   nsAutoString keyValue;
237   mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue);
238   if (!keyValue.IsEmpty() && mContent->GetCurrentDoc()) {
239     nsIContent *keyContent = mContent->GetCurrentDoc()->GetElementById(keyValue);
240     if (keyContent) {
241       nsAutoString keyChar;
242       bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
244       if (!hasKey || keyChar.IsEmpty()) {
245         nsAutoString keyCodeName;
246         keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName);
247         uint32_t charCode =
248           nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
249         if (charCode) {
250           keyChar.Assign(charCode);
251         }
252         else {
253           keyChar.Assign(NS_LITERAL_STRING(" "));
254         }
255       }
257       nsAutoString modifiersStr;
258       keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
259       uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
261       unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
262       [mNativeMenuItem setKeyEquivalentModifierMask:macModifiers];
264       NSString *keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get()
265                                                          length:keyChar.Length()] lowercaseString];
266       if ([keyEquivalent isEqualToString:@" "])
267         [mNativeMenuItem setKeyEquivalent:@""];
268       else
269         [mNativeMenuItem setKeyEquivalent:keyEquivalent];
271       return;
272     }
273   }
275   // if the key was removed, clear the key
276   [mNativeMenuItem setKeyEquivalent:@""];
278   NS_OBJC_END_TRY_ABORT_BLOCK;
282 // nsChangeObserver
285 void
286 nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute)
288   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
290   if (!aContent)
291     return;
292   
293   if (aContent == mContent) { // our own content node changed
294     if (aAttribute == nsGkAtoms::checked) {
295       // if we're a radio menu, uncheck our sibling radio items. No need to
296       // do any of this if we're just a normal check menu.
297       if (mType == eRadioMenuItemType) {
298         if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
299                                   nsGkAtoms::_true, eCaseMatters))
300           UncheckRadioSiblings(mContent);
301       }
302       mMenuParent->SetRebuild(true);
303     }
304     else if (aAttribute == nsGkAtoms::hidden ||
305              aAttribute == nsGkAtoms::collapsed ||
306              aAttribute == nsGkAtoms::label) {
307       mMenuParent->SetRebuild(true);
308     }
309     else if (aAttribute == nsGkAtoms::key) {
310       SetKeyEquiv();
311     }
312     else if (aAttribute == nsGkAtoms::image) {
313       SetupIcon();
314     }
315     else if (aAttribute == nsGkAtoms::disabled) {
316       if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters))
317         [mNativeMenuItem setEnabled:NO];
318       else
319         [mNativeMenuItem setEnabled:YES];
320     }
321   }
322   else if (aContent == mCommandContent) {
323     // the only thing that really matters when the menu isn't showing is the
324     // enabled state since it enables/disables keyboard commands
325     if (aAttribute == nsGkAtoms::disabled) {
326       // first we sync our menu item DOM node with the command DOM node
327       nsAutoString commandDisabled;
328       nsAutoString menuDisabled;
329       aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled);
330       mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled);
331       if (!commandDisabled.Equals(menuDisabled)) {
332         // The menu's disabled state needs to be updated to match the command.
333         if (commandDisabled.IsEmpty()) 
334           mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
335         else
336           mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true);
337       }
338       // now we sync our native menu item with the command DOM node
339       if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters))
340         [mNativeMenuItem setEnabled:NO];
341       else
342         [mNativeMenuItem setEnabled:YES];
343     }
344   }
346   NS_OBJC_END_TRY_ABORT_BLOCK;
349 void nsMenuItemX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer)
351   if (aChild == mCommandContent) {
352     mMenuGroupOwner->UnregisterForContentChanges(mCommandContent);
353     mCommandContent = nullptr;
354   }
356   mMenuParent->SetRebuild(true);
359 void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer,
360                                          nsIContent *aChild)
362   mMenuParent->SetRebuild(true);
365 void nsMenuItemX::SetupIcon()
367   if (mIcon)
368     mIcon->SetupIcon();