1 /* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
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/. */
6 #import <Cocoa/Cocoa.h>
8 #include "NativeMenuMac.h"
10 #include "mozilla/Assertions.h"
11 #include "mozilla/AutoRestore.h"
12 #include "mozilla/BasicEvents.h"
13 #include "mozilla/LookAndFeel.h"
14 #include "mozilla/dom/Document.h"
15 #include "mozilla/dom/Element.h"
17 #include "MOZMenuOpeningCoordinator.h"
18 #include "nsISupports.h"
19 #include "nsGkAtoms.h"
20 #include "nsMenuGroupOwnerX.h"
21 #include "nsMenuItemX.h"
22 #include "nsMenuUtilsX.h"
23 #include "nsNativeThemeColors.h"
24 #include "nsObjCExceptions.h"
25 #include "nsThreadUtils.h"
26 #include "PresShell.h"
27 #include "nsCocoaUtils.h"
29 #include "nsPresContext.h"
30 #include "nsDeviceContext.h"
31 #include "nsCocoaFeatures.h"
39 NativeMenuMac::NativeMenuMac(dom::Element* aElement)
40 : mElement(aElement), mContainerStatusBarItem(nil) {
41 MOZ_RELEASE_ASSERT(aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup));
42 mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, nullptr);
43 mMenu = MakeRefPtr<nsMenuX>(nullptr, mMenuGroupOwner, aElement);
44 mMenu->SetObserver(this);
45 mMenu->SetIconListener(this);
49 NativeMenuMac::~NativeMenuMac() {
50 mMenu->DetachFromGroupOwnerRecursive();
51 mMenu->ClearObserver();
52 mMenu->ClearIconListener();
55 static void UpdateMenu(nsMenuX* aMenu) {
59 uint32_t itemCount = aMenu->GetItemCount();
60 for (uint32_t i = 0; i < itemCount; i++) {
61 nsMenuX::MenuChild menuObject = *aMenu->GetItemAt(i);
62 if (menuObject.is<RefPtr<nsMenuX>>()) {
63 UpdateMenu(menuObject.as<RefPtr<nsMenuX>>());
68 void NativeMenuMac::MenuWillOpen() {
69 // Force an update on the mMenu by faking an open/close on all of
71 UpdateMenu(mMenu.get());
74 bool NativeMenuMac::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
75 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
77 NSMenu* menu = mMenu->NativeNSMenu();
79 nsMenuUtilsX::CheckNativeMenuConsistency(menu);
81 NSString* locationString =
82 [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
83 length:aIndexString.Length()];
84 NSMenuItem* item = nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
86 // We can't perform an action on an item with a submenu, that will raise
87 // an obj-c exception.
88 if (item && !item.hasSubmenu) {
89 NSMenu* parent = item.menu;
91 // NSLog(@"Performing action for native menu item titled: %@\n",
92 // [[currentSubmenu itemAtIndex:targetIndex] title]);
93 mozilla::AutoRestore<bool> autoRestore(
94 nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
95 nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
96 [parent performActionForItemAtIndex:[parent indexOfItem:item]];
103 NS_OBJC_END_TRY_ABORT_BLOCK;
106 void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
107 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
109 NSString* locationString =
110 [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
111 length:aIndexString.Length()];
112 NSArray<NSString*>* indexes = [locationString componentsSeparatedByString:@"|"];
113 RefPtr<nsMenuX> currentMenu = mMenu.get();
115 // now find the correct submenu
116 unsigned int indexCount = indexes.count;
117 for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
118 int targetIndex = [indexes objectAtIndex:i].intValue;
120 uint32_t length = currentMenu->GetItemCount();
121 for (unsigned int j = 0; j < length; j++) {
122 Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
126 RefPtr<nsIContent> content = targetMenu->match(
127 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
128 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
129 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
131 if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
132 currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
139 // fake open/close to cause lazy update to happen
140 currentMenu->MenuOpened();
141 currentMenu->MenuClosed();
143 NS_OBJC_END_TRY_ABORT_BLOCK;
146 void NativeMenuMac::IconUpdated() {
147 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
149 if (mContainerStatusBarItem) {
150 NSImage* menuImage = mMenu->NativeNSMenuItem().image;
152 [menuImage setTemplate:YES];
154 mContainerStatusBarItem.button.image = menuImage;
157 NS_OBJC_END_TRY_ABORT_BLOCK;
160 void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
161 mContainerStatusBarItem = aItem;
165 void NativeMenuMac::Dump() {
166 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
169 nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
171 NS_OBJC_END_TRY_ABORT_BLOCK;
174 void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
175 if (aPopupElement == mElement) {
179 // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
180 // one of the observer notifications destroys us.
181 RefPtr<NativeMenuMac> kungFuDeathGrip(this);
183 for (NativeMenu::Observer* observer : mObservers.Clone()) {
184 observer->OnNativeSubMenuWillOpen(aPopupElement);
188 void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
189 // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
190 // one of the observer notifications destroys us.
191 RefPtr<NativeMenuMac> kungFuDeathGrip(this);
193 for (NativeMenu::Observer* observer : mObservers.Clone()) {
194 if (aPopupElement == mElement) {
195 observer->OnNativeMenuOpened();
197 observer->OnNativeSubMenuDidOpen(aPopupElement);
202 void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
203 dom::Element* aMenuItemElement) {
204 // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
205 // one of the observer notifications destroys us.
206 RefPtr<NativeMenuMac> kungFuDeathGrip(this);
208 for (NativeMenu::Observer* observer : mObservers.Clone()) {
209 observer->OnNativeMenuWillActivateItem(aMenuItemElement);
213 void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
214 // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
215 // one of the observer notifications destroys us.
216 RefPtr<NativeMenuMac> kungFuDeathGrip(this);
218 for (NativeMenu::Observer* observer : mObservers.Clone()) {
219 if (aPopupElement == mElement) {
220 observer->OnNativeMenuClosed();
222 observer->OnNativeSubMenuClosed(aPopupElement);
227 static NSView* NativeViewForFrame(nsIFrame* aFrame) {
228 nsIWidget* widget = aFrame->GetNearestWidget();
229 return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
232 static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
233 nsIFrame* f = aContent->GetPrimaryFrame();
237 return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
240 void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
241 bool aIsContextMenu) {
242 nsPresContext* pc = aClickedFrame->PresContext();
243 auto cssToDesktopScale =
244 pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
245 const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
247 mMenu->PopupShowingEventWasSentAndApprovedExternally();
249 NSMenu* menu = mMenu->NativeNSMenu();
250 NSView* view = NativeViewForFrame(aClickedFrame);
251 NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
252 NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
254 // Let the MOZMenuOpeningCoordinator do the actual opening, so that this ShowAsContextMenu call
255 // does not spawn a nested event loop, which would be surprising to our callers.
256 mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance asynchronouslyOpenMenu:menu
257 atScreenPosition:locationOnScreen
259 withAppearance:appearance
260 asContextMenu:aIsContextMenu];
263 bool NativeMenuMac::Close() {
264 if (mOpeningHandle) {
265 // In case the menu was trying to open, but this Close() call interrupted it, cancel opening.
266 [MOZMenuOpeningCoordinator.sharedInstance cancelAsynchronousOpening:mOpeningHandle];
268 return mMenu->Close();
271 RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(dom::Element* aElement) {
272 nsTArray<RefPtr<dom::Element>> submenuChain;
273 RefPtr<dom::Element> currentElement = aElement->GetParentElement();
274 while (currentElement && currentElement != mElement) {
275 if (currentElement->IsXULElement(nsGkAtoms::menu)) {
276 submenuChain.AppendElement(currentElement);
278 currentElement = currentElement->GetParentElement();
280 if (!currentElement) {
281 // aElement was not a descendent of mElement. Refuse to activate the item.
285 // Traverse submenuChain from shallow to deep, to find the nsMenuX that contains aElement.
286 submenuChain.Reverse();
287 RefPtr<nsMenuX> menu = mMenu;
288 for (const auto& submenu : submenuChain) {
289 if (!menu->IsOpenForGecko()) {
290 // Refuse to descend into closed menus.
293 Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
294 if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
295 // Couldn't find submenu.
298 menu = menuChild->as<RefPtr<nsMenuX>>();
301 if (!menu->IsOpenForGecko()) {
302 // Refuse to descend into closed menus.
308 static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
309 NSEventModifierFlags flags = 0;
310 if (aModifiers & MODIFIER_CONTROL) {
311 flags |= NSEventModifierFlagControl;
313 if (aModifiers & MODIFIER_ALT) {
314 flags |= NSEventModifierFlagOption;
316 if (aModifiers & MODIFIER_SHIFT) {
317 flags |= NSEventModifierFlagShift;
319 if (aModifiers & MODIFIER_META) {
320 flags |= NSEventModifierFlagCommand;
325 void NativeMenuMac::ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, int16_t aButton,
327 RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
329 aRv.ThrowInvalidStateError("Menu containing menu item is not open");
332 Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
333 if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
334 aRv.ThrowInvalidStateError("Could not find the supplied menu item");
338 RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
339 if (!item->IsVisible()) {
340 aRv.ThrowInvalidStateError("Menu item is not visible");
344 NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
346 // First, initiate the closing of the NSMenu.
347 // This synchronously calls the menu delegate's menuDidClose handler. So menuDidClose is
348 // what runs first; this matches the order of events for user-initiated menu item activation.
349 // This call doesn't immediately hide the menu; the menu only hides once the stack unwinds
350 // from NSMenu's nested "tracking" event loop.
351 [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
353 // Next, call OnWillActivateItem. This also matches the order of calls that happen when a user
354 // activates a menu item in the real world: -[MenuDelegate menu:willActivateItem:] runs after
356 menu->OnWillActivateItem(nativeItem);
358 // Finally, call ActivateItemAfterClosing. This also mimics the order in the real world:
359 // menuItemHit is called after menu:willActivateItem:.
360 menu->ActivateItemAfterClosing(std::move(item), ConvertModifierFlags(aModifiers), aButton);
362 // Tell our native event loop that it should not process any more work before
363 // unwinding the stack, so that we can get out of the menu's nested event loop
364 // as fast as possible. This was needed to fix spurious failures in tests, where
365 // a call to cancelTrackingWithoutAnimation was ignored if more native events were
366 // processed before the event loop was exited. As a result, the menu stayed open
367 // forever and the test never finished.
368 MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
370 [nativeItem release];
373 void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
374 if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
375 Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
376 if (item && item->is<RefPtr<nsMenuX>>()) {
377 item->as<RefPtr<nsMenuX>>()->MenuOpened();
382 void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
383 if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
384 Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
385 if (item && item->is<RefPtr<nsMenuX>>()) {
386 item->as<RefPtr<nsMenuX>>()->MenuClosed();
391 RefPtr<Element> NativeMenuMac::Element() { return mElement; }
393 } // namespace widget
394 } // namespace mozilla