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 = [[NSMenuItem 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 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);
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);
133 mMenuGroupOwner->UnregisterForContentChanges(mContent);
135 if (mCommandElement) {
136 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
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
151 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
154 mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
158 // update native menu item
159 mNativeMenuItem.state =
160 mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
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,
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);
181 /* the AttributeChanged code will update all the internal state */
184 nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
187 nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
188 bool* preventDefaultCalled) {
190 return NS_ERROR_FAILURE;
193 // get owner document for content
194 nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
198 RefPtr<Event> event =
199 parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
201 NS_WARNING("Failed to create Event");
202 return rv.StealNSResult();
204 event->InitEvent(eventName, true, true);
206 // mark DOM event as trusted
207 event->SetTrusted(true);
210 *preventDefaultCalled =
211 mContent->DispatchEvent(*event, CallerType::System, rv);
213 NS_WARNING("Failed to send DOM event via EventTarget");
214 return rv.StealNSResult();
220 // Walk the sibling list looking for nodes with the same name and
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
229 nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
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,
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);
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);
266 nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
268 keyChar.Assign(charCode);
270 keyChar.AssignLiteral(u" ");
274 nsAutoString modifiersStr;
275 keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
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 = @"";
289 mNativeMenuItem.keyEquivalent = keyEquivalent;
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());
313 void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
314 nsIContent* aContent,
315 nsAtom* aAttribute) {
316 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
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,
329 nsGkAtoms::_true, eCaseMatters)) {
330 UncheckRadioSiblings(mContent);
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),
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);
352 } else if (aAttribute == nsGkAtoms::key) {
354 } else if (aAttribute == nsGkAtoms::image) {
356 } else if (aAttribute == nsGkAtoms::disabled) {
357 mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
358 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
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);
376 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
377 commandDisabled, true);
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,
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,
398 nsIContent* aPreviousSibling) {
399 MOZ_RELEASE_ASSERT(mMenuGroupOwner);
400 MOZ_RELEASE_ASSERT(mMenuParent);
402 if (aChild == mCommandElement) {
403 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
404 mCommandElement = nullptr;
406 if (IsMenuStructureElement(aChild)) {
407 mMenuParent->SetRebuild(true);
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
419 if (IsMenuStructureElement(aChild)) {
420 mMenuParent->SetRebuild(true);
424 void nsMenuItemX::SetupIcon() {
425 if (mType != eRegularMenuItemType) {
426 // Don't support icons on checkbox and radio menuitems, for consistency with
431 mIcon->SetupIcon(mContent);
432 mNativeMenuItem.image = mIcon->GetIconImage();
435 void nsMenuItemX::IconUpdated() {
436 mNativeMenuItem.image = mIcon->GetIconImage();