summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsMenuItemX.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsMenuItemX.mm')
-rw-r--r--widget/cocoa/nsMenuItemX.mm449
1 files changed, 449 insertions, 0 deletions
diff --git a/widget/cocoa/nsMenuItemX.mm b/widget/cocoa/nsMenuItemX.mm
new file mode 100644
index 0000000000..26241217dc
--- /dev/null
+++ b/widget/cocoa/nsMenuItemX.mm
@@ -0,0 +1,449 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMenuItemX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemIconX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+
+#include "nsObjCExceptions.h"
+
+#include "nsCOMPtr.h"
+#include "nsGkAtoms.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+
+using mozilla::dom::CallerType;
+using mozilla::dom::Event;
+
+nsMenuItemX::nsMenuItemX(nsMenuX* aParent, const nsString& aLabel,
+ EMenuItemType aItemType,
+ nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
+ : mContent(aNode),
+ mType(aItemType),
+ mMenuParent(aParent),
+ mMenuGroupOwner(aMenuGroupOwner) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsMenuItemX);
+
+ MOZ_RELEASE_ASSERT(mContent->IsElement(),
+ "nsMenuItemX should only be created for elements");
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
+
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ dom::Document* doc = mContent->GetUncomposedDoc();
+
+ // if we have a command associated with this menu item, register for changes
+ // to the command DOM node
+ if (doc) {
+ nsAutoString ourCommand;
+ mContent->AsElement()->GetAttr(nsGkAtoms::command, ourCommand);
+
+ if (!ourCommand.IsEmpty()) {
+ dom::Element* commandElement = doc->GetElementById(ourCommand);
+
+ if (commandElement) {
+ mCommandElement = commandElement;
+ // register to observe the command DOM element
+ mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this);
+ }
+ }
+ }
+
+ // decide enabled state based on command content if it exists, otherwise do it
+ // based on our own content
+ bool isEnabled;
+ if (mCommandElement) {
+ isEnabled = !mCommandElement->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ } else {
+ isEnabled = !mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ }
+
+ // set up the native menu item
+ if (mType == eSeparatorMenuItemType) {
+ mNativeMenuItem = [[NSMenuItem separatorItem] retain];
+ } else {
+ NSString* newCocoaLabelString =
+ nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
+ mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
+ action:nil
+ keyEquivalent:@""];
+
+ mIsChecked = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters);
+
+ mNativeMenuItem.enabled = isEnabled;
+ mNativeMenuItem.state =
+ mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
+
+ SetKeyEquiv();
+ }
+
+ mIcon = MakeUnique<nsMenuItemIconX>(this);
+
+ mIsVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ // All menu items other than the "Copy" menu item share the same target and
+ // action, and are differentiated be a unique (representedObject, tag) pair.
+ // The "Copy" menu item is a special case that requires a macOS-default
+ // action of `copy:` and a default target in order for the "Edit" menu to be
+ // populated with OS-provided menu items such as the Emoji picker,
+ // especially in multi-language environments (see bug 1478347). Our
+ // application delegate implements `copy:` by simply forwarding it to
+ // [nsMenuBarX::sNativeEventTarget menuItemHit:].
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
+ u"menu_copy"_ns, eCaseMatters)) {
+ mNativeMenuItem.action = @selector(copy:);
+ } else {
+ mNativeMenuItem.action = @selector(menuItemHit:);
+ mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ }
+
+ mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+ mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
+
+ if (mIsVisible) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuItemX::~nsMenuItemX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // autorelease the native menu item so that anything else happening to this
+ // object happens before the native menu item actually dies
+ [mNativeMenuItem autorelease];
+
+ DetachFromGroupOwner();
+
+ MOZ_COUNT_DTOR(nsMenuItemX);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::DetachFromGroupOwner() {
+ if (mMenuGroupOwner) {
+ mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag);
+
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+ if (mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ }
+ }
+
+ mMenuGroupOwner = nullptr;
+}
+
+nsresult nsMenuItemX::SetChecked(bool aIsChecked) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mIsChecked = aIsChecked;
+
+ // update the content model. This will also handle unchecking our siblings
+ // if we are a radiomenu
+ if (mIsChecked) {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ u"true"_ns, true);
+ } else {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ true);
+ }
+
+ // update native menu item
+ mNativeMenuItem.state =
+ mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; }
+
+// Executes the "cached" javaScript command.
+// Returns NS_OK if the command was executed properly, otherwise an error code.
+void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags,
+ int16_t aButton) {
+ // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
+ if (mType == eCheckboxMenuItemType ||
+ (mType == eRadioMenuItemType && !mIsChecked)) {
+ if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::autocheck,
+ nsGkAtoms::_false, eCaseMatters)) {
+ SetChecked(!mIsChecked);
+ }
+ /* the AttributeChanged code will update all the internal state */
+ }
+
+ nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
+}
+
+nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
+ bool* preventDefaultCalled) {
+ if (!mContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get owner document for content
+ nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
+
+ // create DOM event
+ ErrorResult rv;
+ RefPtr<Event> event =
+ parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to create Event");
+ return rv.StealNSResult();
+ }
+ event->InitEvent(eventName, true, true);
+
+ // mark DOM event as trusted
+ event->SetTrusted(true);
+
+ // send DOM event
+ *preventDefaultCalled =
+ mContent->DispatchEvent(*event, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to send DOM event via EventTarget");
+ return rv.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+// Walk the sibling list looking for nodes with the same name and
+// uncheck them all.
+void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) {
+ nsAutoString myGroupName;
+ aCheckedContent->AsElement()->GetAttr(nsGkAtoms::name, myGroupName);
+ if (!myGroupName.Length()) { // no groupname, nothing to do
+ return;
+ }
+
+ nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ // loop over siblings
+ for (nsIContent* sibling = parent->GetFirstChild(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (sibling != aCheckedContent && sibling->IsElement()) { // skip this node
+ // if the current sibling is in the same group, clear it
+ if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
+ myGroupName, eCaseMatters)) {
+ sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ u"false"_ns, true);
+ }
+ }
+ }
+}
+
+void nsMenuItemX::SetKeyEquiv() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Set key shortcut and modifiers
+ nsAutoString keyValue;
+ mContent->AsElement()->GetAttr(nsGkAtoms::key, keyValue);
+
+ if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) {
+ dom::Element* keyContent =
+ mContent->GetUncomposedDoc()->GetElementById(keyValue);
+ if (keyContent) {
+ nsAutoString keyChar;
+ bool hasKey = keyContent->GetAttr(nsGkAtoms::key, keyChar);
+
+ if (!hasKey || keyChar.IsEmpty()) {
+ nsAutoString keyCodeName;
+ keyContent->GetAttr(nsGkAtoms::keycode, keyCodeName);
+ uint32_t charCode =
+ nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
+ if (charCode) {
+ keyChar.Assign(charCode);
+ } else {
+ keyChar.AssignLiteral(u" ");
+ }
+ }
+
+ nsAutoString modifiersStr;
+ keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
+ uint8_t modifiers =
+ nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+
+ unsigned int macModifiers =
+ nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
+ mNativeMenuItem.keyEquivalentModifierMask = macModifiers;
+
+ NSString* keyEquivalent =
+ [[NSString stringWithCharacters:(unichar*)keyChar.get()
+ length:keyChar.Length()] lowercaseString];
+ if ([keyEquivalent isEqualToString:@" "]) {
+ mNativeMenuItem.keyEquivalent = @"";
+ } else {
+ mNativeMenuItem.keyEquivalent = keyEquivalent;
+ }
+
+ return;
+ }
+ }
+
+ // if the key was removed, clear the key
+ mNativeMenuItem.keyEquivalent = @"";
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::Dump(uint32_t aIndent) const {
+ printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this,
+ mType == eSeparatorMenuItemType ? "----"
+ : [mNativeMenuItem.title UTF8String],
+ NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
+}
+
+//
+// nsChangeObserver
+//
+
+void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (!aContent) {
+ return;
+ }
+
+ if (aContent == mContent) { // our own content node changed
+ if (aAttribute == nsGkAtoms::checked) {
+ // if we're a radio menu, uncheck our sibling radio items. No need to
+ // do any of this if we're just a normal check menu.
+ if (mType == eRadioMenuItemType &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::checked,
+ nsGkAtoms::_true, eCaseMatters)) {
+ UncheckRadioSiblings(mContent);
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::hidden ||
+ aAttribute == nsGkAtoms::collapsed) {
+ bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+ if (isVisible != mIsVisible) {
+ mIsVisible = isVisible;
+ RefPtr<nsMenuItemX> self = this;
+ mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self),
+ isVisible);
+ if (mIsVisible) {
+ SetupIcon();
+ }
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::label) {
+ if (mType != eSeparatorMenuItemType) {
+ nsAutoString newLabel;
+ mContent->AsElement()->GetAttr(nsGkAtoms::label, newLabel);
+ mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel);
+ }
+ } else if (aAttribute == nsGkAtoms::key) {
+ SetKeyEquiv();
+ } else if (aAttribute == nsGkAtoms::image) {
+ SetupIcon();
+ } else if (aAttribute == nsGkAtoms::disabled) {
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters);
+ }
+ } else if (aContent == mCommandElement) {
+ // the only thing that really matters when the menu isn't showing is the
+ // enabled state since it enables/disables keyboard commands
+ if (aAttribute == nsGkAtoms::disabled) {
+ // first we sync our menu item DOM node with the command DOM node
+ nsAutoString commandDisabled;
+ nsAutoString menuDisabled;
+ aContent->AsElement()->GetAttr(nsGkAtoms::disabled, commandDisabled);
+ mContent->AsElement()->GetAttr(nsGkAtoms::disabled, menuDisabled);
+ if (!commandDisabled.Equals(menuDisabled)) {
+ // The menu's disabled state needs to be updated to match the command.
+ if (commandDisabled.IsEmpty()) {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::disabled, true);
+ } else {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandDisabled, true);
+ }
+ }
+ // now we sync our native menu item with the command DOM node
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters);
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool IsMenuStructureElement(nsIContent* aContent) {
+ return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem,
+ nsGkAtoms::menuseparator);
+}
+
+void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ MOZ_RELEASE_ASSERT(mMenuGroupOwner);
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ if (aChild == mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ mCommandElement = nullptr;
+ }
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) {
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ // The child node could come from the custom element that is for display, so
+ // only rebuild the menu if the child is related to the structure of the
+ // menu.
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::SetupIcon() {
+ if (mType != eRegularMenuItemType) {
+ // Don't support icons on checkbox and radio menuitems, for consistency with
+ // Windows & Linux.
+ return;
+ }
+
+ mIcon->SetupIcon(mContent);
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
+
+void nsMenuItemX::IconUpdated() {
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}