summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/NativeMenuMac.mm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /widget/cocoa/NativeMenuMac.mm
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/cocoa/NativeMenuMac.mm')
-rw-r--r--widget/cocoa/NativeMenuMac.mm394
1 files changed, 394 insertions, 0 deletions
diff --git a/widget/cocoa/NativeMenuMac.mm b/widget/cocoa/NativeMenuMac.mm
new file mode 100644
index 0000000000..061d6c7ad9
--- /dev/null
+++ b/widget/cocoa/NativeMenuMac.mm
@@ -0,0 +1,394 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "NativeMenuMac.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+
+#include "MOZMenuOpeningCoordinator.h"
+#include "nsISupports.h"
+#include "nsGkAtoms.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsNativeThemeColors.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+#include "PresShell.h"
+#include "nsCocoaUtils.h"
+#include "nsIFrame.h"
+#include "nsPresContext.h"
+#include "nsDeviceContext.h"
+#include "nsCocoaFeatures.h"
+
+namespace mozilla {
+
+using dom::Element;
+
+namespace widget {
+
+NativeMenuMac::NativeMenuMac(dom::Element* aElement)
+ : mElement(aElement), mContainerStatusBarItem(nil) {
+ MOZ_RELEASE_ASSERT(aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup));
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, nullptr);
+ mMenu = MakeRefPtr<nsMenuX>(nullptr, mMenuGroupOwner, aElement);
+ mMenu->SetObserver(this);
+ mMenu->SetIconListener(this);
+ mMenu->SetupIcon();
+}
+
+NativeMenuMac::~NativeMenuMac() {
+ mMenu->DetachFromGroupOwnerRecursive();
+ mMenu->ClearObserver();
+ mMenu->ClearIconListener();
+}
+
+static void UpdateMenu(nsMenuX* aMenu) {
+ aMenu->MenuOpened();
+ aMenu->MenuClosed();
+
+ uint32_t itemCount = aMenu->GetItemCount();
+ for (uint32_t i = 0; i < itemCount; i++) {
+ nsMenuX::MenuChild menuObject = *aMenu->GetItemAt(i);
+ if (menuObject.is<RefPtr<nsMenuX>>()) {
+ UpdateMenu(menuObject.as<RefPtr<nsMenuX>>());
+ }
+ }
+}
+
+void NativeMenuMac::MenuWillOpen() {
+ // Force an update on the mMenu by faking an open/close on all of
+ // its submenus.
+ UpdateMenu(mMenu.get());
+}
+
+bool NativeMenuMac::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+
+ nsMenuUtilsX::CheckNativeMenuConsistency(menu);
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSMenuItem* item = nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
+
+ // We can't perform an action on an item with a submenu, that will raise
+ // an obj-c exception.
+ if (item && !item.hasSubmenu) {
+ NSMenu* parent = item.menu;
+ if (parent) {
+ // NSLog(@"Performing action for native menu item titled: %@\n",
+ // [[currentSubmenu itemAtIndex:targetIndex] title]);
+ mozilla::AutoRestore<bool> autoRestore(
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
+ [parent performActionForItemAtIndex:[parent indexOfItem:item]];
+ return true;
+ }
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray<NSString*>* indexes = [locationString componentsSeparatedByString:@"|"];
+ RefPtr<nsMenuX> currentMenu = mMenu.get();
+
+ // now find the correct submenu
+ unsigned int indexCount = indexes.count;
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ int targetIndex = [indexes objectAtIndex:i].intValue;
+ int visible = 0;
+ uint32_t length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ break;
+ }
+ }
+ }
+ }
+
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::IconUpdated() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mContainerStatusBarItem) {
+ NSImage* menuImage = mMenu->NativeNSMenuItem().image;
+ if (menuImage) {
+ [menuImage setTemplate:YES];
+ }
+ mContainerStatusBarItem.button.image = menuImage;
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
+ mContainerStatusBarItem = aItem;
+ IconUpdated();
+}
+
+void NativeMenuMac::Dump() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenu->Dump(0);
+ nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
+ if (aPopupElement == mElement) {
+ return;
+ }
+
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeSubMenuWillOpen(aPopupElement);
+ }
+}
+
+void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuOpened();
+ } else {
+ observer->OnNativeSubMenuDidOpen(aPopupElement);
+ }
+ }
+}
+
+void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
+ dom::Element* aMenuItemElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeMenuWillActivateItem(aMenuItemElement);
+ }
+}
+
+void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuClosed();
+ } else {
+ observer->OnNativeSubMenuClosed(aPopupElement);
+ }
+ }
+}
+
+static NSView* NativeViewForFrame(nsIFrame* aFrame) {
+ nsIWidget* widget = aFrame->GetNearestWidget();
+ return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
+}
+
+static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
+ nsIFrame* f = aContent->GetPrimaryFrame();
+ if (!f) {
+ return nil;
+ }
+ return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
+}
+
+void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
+ bool aIsContextMenu) {
+ nsPresContext* pc = aClickedFrame->PresContext();
+ auto cssToDesktopScale =
+ pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
+ const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
+
+ mMenu->PopupShowingEventWasSentAndApprovedExternally();
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+ NSView* view = NativeViewForFrame(aClickedFrame);
+ NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
+ NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
+
+ // Let the MOZMenuOpeningCoordinator do the actual opening, so that this ShowAsContextMenu call
+ // does not spawn a nested event loop, which would be surprising to our callers.
+ mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance asynchronouslyOpenMenu:menu
+ atScreenPosition:locationOnScreen
+ forView:view
+ withAppearance:appearance
+ asContextMenu:aIsContextMenu];
+}
+
+bool NativeMenuMac::Close() {
+ if (mOpeningHandle) {
+ // In case the menu was trying to open, but this Close() call interrupted it, cancel opening.
+ [MOZMenuOpeningCoordinator.sharedInstance cancelAsynchronousOpening:mOpeningHandle];
+ }
+ return mMenu->Close();
+}
+
+RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(dom::Element* aElement) {
+ nsTArray<RefPtr<dom::Element>> submenuChain;
+ RefPtr<dom::Element> currentElement = aElement->GetParentElement();
+ while (currentElement && currentElement != mElement) {
+ if (currentElement->IsXULElement(nsGkAtoms::menu)) {
+ submenuChain.AppendElement(currentElement);
+ }
+ currentElement = currentElement->GetParentElement();
+ }
+ if (!currentElement) {
+ // aElement was not a descendent of mElement. Refuse to activate the item.
+ return nullptr;
+ }
+
+ // Traverse submenuChain from shallow to deep, to find the nsMenuX that contains aElement.
+ submenuChain.Reverse();
+ RefPtr<nsMenuX> menu = mMenu;
+ for (const auto& submenu : submenuChain) {
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
+ if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
+ // Couldn't find submenu.
+ return nullptr;
+ }
+ menu = menuChild->as<RefPtr<nsMenuX>>();
+ }
+
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ return menu;
+}
+
+static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
+ NSEventModifierFlags flags = 0;
+ if (aModifiers & MODIFIER_CONTROL) {
+ flags |= NSEventModifierFlagControl;
+ }
+ if (aModifiers & MODIFIER_ALT) {
+ flags |= NSEventModifierFlagOption;
+ }
+ if (aModifiers & MODIFIER_SHIFT) {
+ flags |= NSEventModifierFlagShift;
+ }
+ if (aModifiers & MODIFIER_META) {
+ flags |= NSEventModifierFlagCommand;
+ }
+ return flags;
+}
+
+void NativeMenuMac::ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, int16_t aButton,
+ ErrorResult& aRv) {
+ RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
+ if (!menu) {
+ aRv.ThrowInvalidStateError("Menu containing menu item is not open");
+ return;
+ }
+ Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
+ if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
+ aRv.ThrowInvalidStateError("Could not find the supplied menu item");
+ return;
+ }
+
+ RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
+ if (!item->IsVisible()) {
+ aRv.ThrowInvalidStateError("Menu item is not visible");
+ return;
+ }
+
+ NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
+
+ // First, initiate the closing of the NSMenu.
+ // This synchronously calls the menu delegate's menuDidClose handler. So menuDidClose is
+ // what runs first; this matches the order of events for user-initiated menu item activation.
+ // This call doesn't immediately hide the menu; the menu only hides once the stack unwinds
+ // from NSMenu's nested "tracking" event loop.
+ [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
+
+ // Next, call OnWillActivateItem. This also matches the order of calls that happen when a user
+ // activates a menu item in the real world: -[MenuDelegate menu:willActivateItem:] runs after
+ // menuDidClose.
+ menu->OnWillActivateItem(nativeItem);
+
+ // Finally, call ActivateItemAfterClosing. This also mimics the order in the real world:
+ // menuItemHit is called after menu:willActivateItem:.
+ menu->ActivateItemAfterClosing(std::move(item), ConvertModifierFlags(aModifiers), aButton);
+
+ // Tell our native event loop that it should not process any more work before
+ // unwinding the stack, so that we can get out of the menu's nested event loop
+ // as fast as possible. This was needed to fix spurious failures in tests, where
+ // a call to cancelTrackingWithoutAnimation was ignored if more native events were
+ // processed before the event loop was exited. As a result, the menu stayed open
+ // forever and the test never finished.
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+
+ [nativeItem release];
+}
+
+void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuOpened();
+ }
+ }
+}
+
+void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuClosed();
+ }
+ }
+}
+
+RefPtr<Element> NativeMenuMac::Element() { return mElement; }
+
+} // namespace widget
+} // namespace mozilla