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 "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;
33 MOZ_COUNT_CTOR(nsMenuItemX);
36 nsMenuItemX::~nsMenuItemX()
38 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
40 // Prevent the icon object from outliving us.
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];
49 mMenuGroupOwner->UnregisterForContentChanges(mContent);
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;
64 mMenuParent = aParent;
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
77 nsAutoString ourCommand;
78 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand);
80 if (!ourCommand.IsEmpty()) {
81 nsIContent *commandElement = doc->GetElementById(ourCommand);
84 mCommandContent = commandElement;
85 // register to observe the command DOM element
86 mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this);
91 // decide enabled state based on command content if it exists, otherwise do it based
95 isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
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];
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));
114 mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem);
116 return NS_ERROR_OUT_OF_MEMORY;
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;
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);
134 // update native menu item
136 [mNativeMenuItem setState:NSOnState];
138 [mNativeMenuItem setState:NSOffState];
142 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
145 EMenuItemType nsMenuItemX::GetMenuItemType()
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 */
163 nsMenuUtilsX::DispatchCommandTo(mContent);
166 nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled)
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);
177 NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument");
178 return NS_ERROR_FAILURE;
182 nsCOMPtr<nsIDOMEvent> event;
183 nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event));
185 NS_WARNING("Failed to create nsIDOMEvent");
188 event->InitEvent(eventName, true, true);
190 // mark DOM event as trusted
191 event->SetTrusted(true);
194 rv = mContent->DispatchEvent(event, preventDefaultCalled);
196 NS_WARNING("Failed to send DOM event via EventTarget");
203 // Walk the sibling list looking for nodes with the same name and
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
212 nsCOMPtr<nsIContent> parent = inCheckedContent->GetParent();
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);
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);
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);
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);
248 nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
250 keyChar.Assign(charCode);
253 keyChar.Assign(NS_LITERAL_STRING(" "));
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:@""];
269 [mNativeMenuItem setKeyEquivalent:keyEquivalent];
275 // if the key was removed, clear the key
276 [mNativeMenuItem setKeyEquivalent:@""];
278 NS_OBJC_END_TRY_ABORT_BLOCK;
286 nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute)
288 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
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);
302 mMenuParent->SetRebuild(true);
304 else if (aAttribute == nsGkAtoms::hidden ||
305 aAttribute == nsGkAtoms::collapsed ||
306 aAttribute == nsGkAtoms::label) {
307 mMenuParent->SetRebuild(true);
309 else if (aAttribute == nsGkAtoms::key) {
312 else if (aAttribute == nsGkAtoms::image) {
315 else if (aAttribute == nsGkAtoms::disabled) {
316 if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters))
317 [mNativeMenuItem setEnabled:NO];
319 [mNativeMenuItem setEnabled:YES];
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);
336 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true);
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];
342 [mNativeMenuItem setEnabled:YES];
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;
356 mMenuParent->SetRebuild(true);
359 void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer,
362 mMenuParent->SetRebuild(true);
365 void nsMenuItemX::SetupIcon()