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/. */ 5 6#include "nsMenuItemX.h" 7#include "nsMenuBarX.h" 8#include "nsMenuX.h" 9#include "nsMenuItemIconX.h" 10#include "nsMenuUtilsX.h" 11#include "nsCocoaUtils.h" 12 13#include "nsObjCExceptions.h" 14 15#include "nsCOMPtr.h" 16#include "nsGkAtoms.h" 17 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" 23 24using namespace mozilla; 25 26using mozilla::dom::Event; 27using mozilla::dom::CallerType; 28 29nsMenuItemX::nsMenuItemX(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType, 30 nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) 31 : mContent(aNode), mType(aItemType), mMenuParent(aParent), mMenuGroupOwner(aMenuGroupOwner) { 32 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 33 34 MOZ_COUNT_CTOR(nsMenuItemX); 35 36 MOZ_RELEASE_ASSERT(mContent->IsElement(), "nsMenuItemX should only be created for elements"); 37 NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!"); 38 39 mMenuGroupOwner->RegisterForContentChanges(mContent, this); 40 41 dom::Document* doc = mContent->GetUncomposedDoc(); 42 43 // if we have a command associated with this menu item, register for changes 44 // to the command DOM node 45 if (doc) { 46 nsAutoString ourCommand; 47 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand); 48 49 if (!ourCommand.IsEmpty()) { 50 dom::Element* commandElement = doc->GetElementById(ourCommand); 51 52 if (commandElement) { 53 mCommandElement = commandElement; 54 // register to observe the command DOM element 55 mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this); 56 } 57 } 58 } 59 60 // decide enabled state based on command content if it exists, otherwise do it based 61 // on our own content 62 bool isEnabled; 63 if (mCommandElement) { 64 isEnabled = !mCommandElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, 65 nsGkAtoms::_true, eCaseMatters); 66 } else { 67 isEnabled = !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, 68 nsGkAtoms::_true, eCaseMatters); 69 } 70 71 // set up the native menu item 72 if (mType == eSeparatorMenuItemType) { 73 mNativeMenuItem = [[NSMenuItem separatorItem] retain]; 74 } else { 75 NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel); 76 mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString 77 action:nil 78 keyEquivalent:@""]; 79 80 mIsChecked = mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, 81 nsGkAtoms::_true, eCaseMatters); 82 83 mNativeMenuItem.enabled = isEnabled; 84 mNativeMenuItem.state = mIsChecked ? NSOnState : NSOffState; 85 86 SetKeyEquiv(); 87 } 88 89 mIcon = MakeUnique<nsMenuItemIconX>(this); 90 91 mIsVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); 92 93 // All menu items share the same target and action, and are differentiated 94 // be a unique (representedObject, tag) pair. 95 mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget; 96 mNativeMenuItem.action = @selector(menuItemHit:); 97 mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject(); 98 mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this); 99 100 if (mIsVisible) { 101 SetupIcon(); 102 } 103 104 NS_OBJC_END_TRY_ABORT_BLOCK; 105} 106 107nsMenuItemX::~nsMenuItemX() { 108 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 109 110 // autorelease the native menu item so that anything else happening to this 111 // object happens before the native menu item actually dies 112 [mNativeMenuItem autorelease]; 113 114 DetachFromGroupOwner(); 115 116 MOZ_COUNT_DTOR(nsMenuItemX); 117 118 NS_OBJC_END_TRY_ABORT_BLOCK; 119} 120 121void nsMenuItemX::DetachFromGroupOwner() { 122 if (mMenuGroupOwner) { 123 mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag); 124 125 if (mContent) { 126 mMenuGroupOwner->UnregisterForContentChanges(mContent); 127 } 128 if (mCommandElement) { 129 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement); 130 } 131 } 132 133 mMenuGroupOwner = nullptr; 134} 135 136nsresult nsMenuItemX::SetChecked(bool aIsChecked) { 137 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 138 139 mIsChecked = aIsChecked; 140 141 // update the content model. This will also handle unchecking our siblings 142 // if we are a radiomenu 143 if (mIsChecked) { 144 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true); 145 } else { 146 mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true); 147 } 148 149 // update native menu item 150 mNativeMenuItem.state = mIsChecked ? NSOnState : NSOffState; 151 152 return NS_OK; 153 154 NS_OBJC_END_TRY_ABORT_BLOCK; 155} 156 157EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; } 158 159// Executes the "cached" javaScript command. 160// Returns NS_OK if the command was executed properly, otherwise an error code. 161void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags, int16_t aButton) { 162 // flip "checked" state if we're a checkbox menu, or an un-checked radio menu 163 if (mType == eCheckboxMenuItemType || (mType == eRadioMenuItemType && !mIsChecked)) { 164 if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, 165 nsGkAtoms::_false, eCaseMatters)) { 166 SetChecked(!mIsChecked); 167 } 168 /* the AttributeChanged code will update all the internal state */ 169 } 170 171 nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton); 172} 173 174nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName, bool* preventDefaultCalled) { 175 if (!mContent) { 176 return NS_ERROR_FAILURE; 177 } 178 179 // get owner document for content 180 nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc(); 181 182 // create DOM event 183 ErrorResult rv; 184 RefPtr<Event> event = parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv); 185 if (rv.Failed()) { 186 NS_WARNING("Failed to create Event"); 187 return rv.StealNSResult(); 188 } 189 event->InitEvent(eventName, true, true); 190 191 // mark DOM event as trusted 192 event->SetTrusted(true); 193 194 // send DOM event 195 *preventDefaultCalled = mContent->DispatchEvent(*event, CallerType::System, rv); 196 if (rv.Failed()) { 197 NS_WARNING("Failed to send DOM event via EventTarget"); 198 return rv.StealNSResult(); 199 } 200 201 return NS_OK; 202} 203 204// Walk the sibling list looking for nodes with the same name and 205// uncheck them all. 206void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) { 207 nsAutoString myGroupName; 208 aCheckedContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName); 209 if (!myGroupName.Length()) { // no groupname, nothing to do 210 return; 211 } 212 213 nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent(); 214 if (!parent) { 215 return; 216 } 217 218 // loop over siblings 219 for (nsIContent* sibling = parent->GetFirstChild(); sibling; 220 sibling = sibling->GetNextSibling()) { 221 if (sibling != aCheckedContent && sibling->IsElement()) { // skip this node 222 // if the current sibling is in the same group, clear it 223 if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, myGroupName, 224 eCaseMatters)) { 225 sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"false"_ns, true); 226 } 227 } 228 } 229} 230 231void nsMenuItemX::SetKeyEquiv() { 232 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 233 234 // Set key shortcut and modifiers 235 nsAutoString keyValue; 236 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); 237 238 if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) { 239 dom::Element* keyContent = mContent->GetUncomposedDoc()->GetElementById(keyValue); 240 if (keyContent) { 241 nsAutoString keyChar; 242 bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar); 243 244 if (!hasKey || keyChar.IsEmpty()) { 245 nsAutoString keyCodeName; 246 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName); 247 uint32_t charCode = nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName); 248 if (charCode) { 249 keyChar.Assign(charCode); 250 } else { 251 keyChar.AssignLiteral(u" "); 252 } 253 } 254 255 nsAutoString modifiersStr; 256 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr); 257 uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr); 258 259 unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers); 260 mNativeMenuItem.keyEquivalentModifierMask = macModifiers; 261 262 NSString* keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get() 263 length:keyChar.Length()] lowercaseString]; 264 if ([keyEquivalent isEqualToString:@" "]) { 265 mNativeMenuItem.keyEquivalent = @""; 266 } else { 267 mNativeMenuItem.keyEquivalent = keyEquivalent; 268 } 269 270 return; 271 } 272 } 273 274 // if the key was removed, clear the key 275 mNativeMenuItem.keyEquivalent = @""; 276 277 NS_OBJC_END_TRY_ABORT_BLOCK; 278} 279 280void nsMenuItemX::Dump(uint32_t aIndent) const { 281 printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this, 282 mType == eSeparatorMenuItemType ? "----" : [mNativeMenuItem.title UTF8String], 283 NS_ConvertUTF16toUTF8(mContent->NodeName()).get()); 284} 285 286// 287// nsChangeObserver 288// 289 290void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent, 291 nsAtom* aAttribute) { 292 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 293 294 if (!aContent) { 295 return; 296 } 297 298 if (aContent == mContent) { // our own content node changed 299 if (aAttribute == nsGkAtoms::checked) { 300 // if we're a radio menu, uncheck our sibling radio items. No need to 301 // do any of this if we're just a normal check menu. 302 if (mType == eRadioMenuItemType && 303 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, 304 nsGkAtoms::_true, eCaseMatters)) { 305 UncheckRadioSiblings(mContent); 306 } 307 mMenuParent->SetRebuild(true); 308 } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) { 309 bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); 310 if (isVisible != mIsVisible) { 311 mIsVisible = isVisible; 312 RefPtr<nsMenuItemX> self = this; 313 mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self), isVisible); 314 if (mIsVisible) { 315 SetupIcon(); 316 } 317 } 318 mMenuParent->SetRebuild(true); 319 } else if (aAttribute == nsGkAtoms::label) { 320 if (mType != eSeparatorMenuItemType) { 321 nsAutoString newLabel; 322 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, newLabel); 323 mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel); 324 } 325 } else if (aAttribute == nsGkAtoms::key) { 326 SetKeyEquiv(); 327 } else if (aAttribute == nsGkAtoms::image) { 328 SetupIcon(); 329 } else if (aAttribute == nsGkAtoms::disabled) { 330 mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs( 331 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); 332 } 333 } else if (aContent == mCommandElement) { 334 // the only thing that really matters when the menu isn't showing is the 335 // enabled state since it enables/disables keyboard commands 336 if (aAttribute == nsGkAtoms::disabled) { 337 // first we sync our menu item DOM node with the command DOM node 338 nsAutoString commandDisabled; 339 nsAutoString menuDisabled; 340 aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled); 341 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled); 342 if (!commandDisabled.Equals(menuDisabled)) { 343 // The menu's disabled state needs to be updated to match the command. 344 if (commandDisabled.IsEmpty()) { 345 mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); 346 } else { 347 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, 348 true); 349 } 350 } 351 // now we sync our native menu item with the command DOM node 352 mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs( 353 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); 354 } 355 } 356 357 NS_OBJC_END_TRY_ABORT_BLOCK; 358} 359 360bool IsMenuStructureElement(nsIContent* aContent) { 361 return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem, 362 nsGkAtoms::menuseparator); 363} 364 365void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer, 366 nsIContent* aChild, nsIContent* aPreviousSibling) { 367 MOZ_RELEASE_ASSERT(mMenuGroupOwner); 368 MOZ_RELEASE_ASSERT(mMenuParent); 369 370 if (aChild == mCommandElement) { 371 mMenuGroupOwner->UnregisterForContentChanges(mCommandElement); 372 mCommandElement = nullptr; 373 } 374 if (IsMenuStructureElement(aChild)) { 375 mMenuParent->SetRebuild(true); 376 } 377} 378 379void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer, 380 nsIContent* aChild) { 381 MOZ_RELEASE_ASSERT(mMenuParent); 382 383 // The child node could come from the custom element that is for display, so 384 // only rebuild the menu if the child is related to the structure of the 385 // menu. 386 if (IsMenuStructureElement(aChild)) { 387 mMenuParent->SetRebuild(true); 388 } 389} 390 391void nsMenuItemX::SetupIcon() { 392 if (mType != eRegularMenuItemType) { 393 // Don't support icons on checkbox and radio menuitems, for consistency with Windows & Linux. 394 return; 395 } 396 397 mIcon->SetupIcon(mContent); 398 mNativeMenuItem.image = mIcon->GetIconImage(); 399} 400 401void nsMenuItemX::IconUpdated() { mNativeMenuItem.image = mIcon->GetIconImage(); } 402