/* -*- 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 #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(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>()) { UpdateMenu(menuObject.as>()); } } } 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(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 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(aIndexString.BeginReading()) length:aIndexString.Length()]; NSArray* indexes = [locationString componentsSeparatedByString:@"|"]; RefPtr 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 targetMenu = currentMenu->GetItemAt(j); if (!targetMenu) { return; } RefPtr content = targetMenu->match( [](const RefPtr& aMenu) { return aMenu->Content(); }, [](const RefPtr& aMenuItem) { return aMenuItem->Content(); }); if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) { visible++; if (targetMenu->is>() && visible == (targetIndex + 1)) { currentMenu = targetMenu->as>(); 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.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 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 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 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 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) { 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]; } 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 NativeMenuMac::GetOpenMenuContainingElement(dom::Element* aElement) { nsTArray> submenuChain; RefPtr 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 menu = mMenu; for (const auto& submenu : submenuChain) { if (!menu->IsOpenForGecko()) { // Refuse to descend into closed menus. return nullptr; } Maybe menuChild = menu->GetItemForElement(submenu); if (!menuChild || !menuChild->is>()) { // Couldn't find submenu. return nullptr; } menu = menuChild->as>(); } 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 menu = GetOpenMenuContainingElement(aItemElement); if (!menu) { aRv.ThrowInvalidStateError("Menu containing menu item is not open"); return; } Maybe child = menu->GetItemForElement(aItemElement); if (!child || !child->is>()) { aRv.ThrowInvalidStateError("Could not find the supplied menu item"); return; } RefPtr item = std::move(child->as>()); 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 menu = GetOpenMenuContainingElement(aMenuElement)) { Maybe item = menu->GetItemForElement(aMenuElement); if (item && item->is>()) { item->as>()->MenuOpened(); } } } void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) { if (RefPtr menu = GetOpenMenuContainingElement(aMenuElement)) { Maybe item = menu->GetItemForElement(aMenuElement); if (item && item->is>()) { item->as>()->MenuClosed(); } } } RefPtr NativeMenuMac::Element() { return mElement; } } // namespace widget } // namespace mozilla