michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsMenuItemX.h" michael@0: #include "nsMenuBarX.h" michael@0: #include "nsMenuX.h" michael@0: #include "nsMenuItemIconX.h" michael@0: #include "nsMenuUtilsX.h" michael@0: #include "nsCocoaUtils.h" michael@0: michael@0: #include "nsObjCExceptions.h" michael@0: michael@0: #include "nsCOMPtr.h" michael@0: #include "nsGkAtoms.h" michael@0: michael@0: #include "mozilla/dom/Element.h" michael@0: #include "nsIWidget.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIDOMDocument.h" michael@0: #include "nsIDOMElement.h" michael@0: #include "nsIDOMEvent.h" michael@0: michael@0: nsMenuItemX::nsMenuItemX() michael@0: { michael@0: mType = eRegularMenuItemType; michael@0: mNativeMenuItem = nil; michael@0: mMenuParent = nullptr; michael@0: mMenuGroupOwner = nullptr; michael@0: mIsChecked = false; michael@0: michael@0: MOZ_COUNT_CTOR(nsMenuItemX); michael@0: } michael@0: michael@0: nsMenuItemX::~nsMenuItemX() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: // Prevent the icon object from outliving us. michael@0: if (mIcon) michael@0: mIcon->Destroy(); michael@0: michael@0: // autorelease the native menu item so that anything else happening to this michael@0: // object happens before the native menu item actually dies michael@0: [mNativeMenuItem autorelease]; michael@0: michael@0: if (mContent) michael@0: mMenuGroupOwner->UnregisterForContentChanges(mContent); michael@0: if (mCommandContent) michael@0: mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); michael@0: michael@0: MOZ_COUNT_DTOR(nsMenuItemX); michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType, michael@0: nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: mType = aItemType; michael@0: mMenuParent = aParent; michael@0: mContent = aNode; michael@0: michael@0: mMenuGroupOwner = aMenuGroupOwner; michael@0: NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!"); michael@0: michael@0: mMenuGroupOwner->RegisterForContentChanges(mContent, this); michael@0: michael@0: nsIDocument *doc = mContent->GetCurrentDoc(); michael@0: michael@0: // if we have a command associated with this menu item, register for changes michael@0: // to the command DOM node michael@0: if (doc) { michael@0: nsAutoString ourCommand; michael@0: mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand); michael@0: michael@0: if (!ourCommand.IsEmpty()) { michael@0: nsIContent *commandElement = doc->GetElementById(ourCommand); michael@0: michael@0: if (commandElement) { michael@0: mCommandContent = commandElement; michael@0: // register to observe the command DOM element michael@0: mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // decide enabled state based on command content if it exists, otherwise do it based michael@0: // on our own content michael@0: bool isEnabled; michael@0: if (mCommandContent) michael@0: isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); michael@0: else michael@0: isEnabled = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); michael@0: michael@0: // set up the native menu item michael@0: if (mType == eSeparatorMenuItemType) { michael@0: mNativeMenuItem = [[NSMenuItem separatorItem] retain]; michael@0: } michael@0: else { michael@0: NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel); michael@0: mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""]; michael@0: michael@0: [mNativeMenuItem setEnabled:(BOOL)isEnabled]; michael@0: michael@0: SetChecked(mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, michael@0: nsGkAtoms::_true, eCaseMatters)); michael@0: SetKeyEquiv(); michael@0: } michael@0: michael@0: mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); michael@0: if (!mIcon) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: nsresult nsMenuItemX::SetChecked(bool aIsChecked) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: mIsChecked = aIsChecked; michael@0: michael@0: // update the content model. This will also handle unchecking our siblings michael@0: // if we are a radiomenu michael@0: mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, michael@0: mIsChecked ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false"), true); michael@0: michael@0: // update native menu item michael@0: if (mIsChecked) michael@0: [mNativeMenuItem setState:NSOnState]; michael@0: else michael@0: [mNativeMenuItem setState:NSOffState]; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: EMenuItemType nsMenuItemX::GetMenuItemType() michael@0: { michael@0: return mType; michael@0: } michael@0: michael@0: // Executes the "cached" javaScript command. michael@0: // Returns NS_OK if the command was executed properly, otherwise an error code. michael@0: void nsMenuItemX::DoCommand() michael@0: { michael@0: // flip "checked" state if we're a checkbox menu, or an un-checked radio menu michael@0: if (mType == eCheckboxMenuItemType || michael@0: (mType == eRadioMenuItemType && !mIsChecked)) { michael@0: if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, michael@0: nsGkAtoms::_false, eCaseMatters)) michael@0: SetChecked(!mIsChecked); michael@0: /* the AttributeChanged code will update all the internal state */ michael@0: } michael@0: michael@0: nsMenuUtilsX::DispatchCommandTo(mContent); michael@0: } michael@0: michael@0: nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled) michael@0: { michael@0: if (!mContent) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: // get owner document for content michael@0: nsCOMPtr parentDoc = mContent->OwnerDoc(); michael@0: michael@0: // get interface for creating DOM events from content owner document michael@0: nsCOMPtr domDoc = do_QueryInterface(parentDoc); michael@0: if (!domDoc) { michael@0: NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: // create DOM event michael@0: nsCOMPtr event; michael@0: nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event)); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Failed to create nsIDOMEvent"); michael@0: return rv; michael@0: } michael@0: event->InitEvent(eventName, true, true); michael@0: michael@0: // mark DOM event as trusted michael@0: event->SetTrusted(true); michael@0: michael@0: // send DOM event michael@0: rv = mContent->DispatchEvent(event, preventDefaultCalled); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Failed to send DOM event via EventTarget"); michael@0: return rv; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Walk the sibling list looking for nodes with the same name and michael@0: // uncheck them all. michael@0: void nsMenuItemX::UncheckRadioSiblings(nsIContent* inCheckedContent) michael@0: { michael@0: nsAutoString myGroupName; michael@0: inCheckedContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName); michael@0: if (!myGroupName.Length()) // no groupname, nothing to do michael@0: return; michael@0: michael@0: nsCOMPtr parent = inCheckedContent->GetParent(); michael@0: if (!parent) michael@0: return; michael@0: michael@0: // loop over siblings michael@0: uint32_t count = parent->GetChildCount(); michael@0: for (uint32_t i = 0; i < count; i++) { michael@0: nsIContent *sibling = parent->GetChildAt(i); michael@0: if (sibling) { michael@0: if (sibling != inCheckedContent) { // skip this node michael@0: // if the current sibling is in the same group, clear it michael@0: if (sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, michael@0: myGroupName, eCaseMatters)) michael@0: sibling->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("false"), true); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void nsMenuItemX::SetKeyEquiv() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: // Set key shortcut and modifiers michael@0: nsAutoString keyValue; michael@0: mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); michael@0: if (!keyValue.IsEmpty() && mContent->GetCurrentDoc()) { michael@0: nsIContent *keyContent = mContent->GetCurrentDoc()->GetElementById(keyValue); michael@0: if (keyContent) { michael@0: nsAutoString keyChar; michael@0: bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar); michael@0: michael@0: if (!hasKey || keyChar.IsEmpty()) { michael@0: nsAutoString keyCodeName; michael@0: keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName); michael@0: uint32_t charCode = michael@0: nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName); michael@0: if (charCode) { michael@0: keyChar.Assign(charCode); michael@0: } michael@0: else { michael@0: keyChar.Assign(NS_LITERAL_STRING(" ")); michael@0: } michael@0: } michael@0: michael@0: nsAutoString modifiersStr; michael@0: keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr); michael@0: uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr); michael@0: michael@0: unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers); michael@0: [mNativeMenuItem setKeyEquivalentModifierMask:macModifiers]; michael@0: michael@0: NSString *keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get() michael@0: length:keyChar.Length()] lowercaseString]; michael@0: if ([keyEquivalent isEqualToString:@" "]) michael@0: [mNativeMenuItem setKeyEquivalent:@""]; michael@0: else michael@0: [mNativeMenuItem setKeyEquivalent:keyEquivalent]; michael@0: michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // if the key was removed, clear the key michael@0: [mNativeMenuItem setKeyEquivalent:@""]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: // michael@0: // nsChangeObserver michael@0: // michael@0: michael@0: void michael@0: nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (!aContent) michael@0: return; michael@0: michael@0: if (aContent == mContent) { // our own content node changed michael@0: if (aAttribute == nsGkAtoms::checked) { michael@0: // if we're a radio menu, uncheck our sibling radio items. No need to michael@0: // do any of this if we're just a normal check menu. michael@0: if (mType == eRadioMenuItemType) { michael@0: if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, michael@0: nsGkAtoms::_true, eCaseMatters)) michael@0: UncheckRadioSiblings(mContent); michael@0: } michael@0: mMenuParent->SetRebuild(true); michael@0: } michael@0: else if (aAttribute == nsGkAtoms::hidden || michael@0: aAttribute == nsGkAtoms::collapsed || michael@0: aAttribute == nsGkAtoms::label) { michael@0: mMenuParent->SetRebuild(true); michael@0: } michael@0: else if (aAttribute == nsGkAtoms::key) { michael@0: SetKeyEquiv(); michael@0: } michael@0: else if (aAttribute == nsGkAtoms::image) { michael@0: SetupIcon(); michael@0: } michael@0: else if (aAttribute == nsGkAtoms::disabled) { michael@0: if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) michael@0: [mNativeMenuItem setEnabled:NO]; michael@0: else michael@0: [mNativeMenuItem setEnabled:YES]; michael@0: } michael@0: } michael@0: else if (aContent == mCommandContent) { michael@0: // the only thing that really matters when the menu isn't showing is the michael@0: // enabled state since it enables/disables keyboard commands michael@0: if (aAttribute == nsGkAtoms::disabled) { michael@0: // first we sync our menu item DOM node with the command DOM node michael@0: nsAutoString commandDisabled; michael@0: nsAutoString menuDisabled; michael@0: aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled); michael@0: mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled); michael@0: if (!commandDisabled.Equals(menuDisabled)) { michael@0: // The menu's disabled state needs to be updated to match the command. michael@0: if (commandDisabled.IsEmpty()) michael@0: mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); michael@0: else michael@0: mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true); michael@0: } michael@0: // now we sync our native menu item with the command DOM node michael@0: if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) michael@0: [mNativeMenuItem setEnabled:NO]; michael@0: else michael@0: [mNativeMenuItem setEnabled:YES]; michael@0: } michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: void nsMenuItemX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer) michael@0: { michael@0: if (aChild == mCommandContent) { michael@0: mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); michael@0: mCommandContent = nullptr; michael@0: } michael@0: michael@0: mMenuParent->SetRebuild(true); michael@0: } michael@0: michael@0: void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer, michael@0: nsIContent *aChild) michael@0: { michael@0: mMenuParent->SetRebuild(true); michael@0: } michael@0: michael@0: void nsMenuItemX::SetupIcon() michael@0: { michael@0: if (mIcon) michael@0: mIcon->SetupIcon(); michael@0: }