Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / widget / cocoa / NativeMenuMac.mm
blob4efa48c53f59800a562e468ac69027f31d364774
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"
28 #include "nsIFrame.h"
29 #include "nsPresContext.h"
30 #include "nsDeviceContext.h"
32 namespace mozilla {
34 using dom::Element;
36 namespace widget {
38 NativeMenuMac::NativeMenuMac(dom::Element* aElement)
39     : mElement(aElement), mContainerStatusBarItem(nil) {
40   MOZ_RELEASE_ASSERT(
41       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);
46   mMenu->SetupIcon();
49 NativeMenuMac::~NativeMenuMac() {
50   mMenu->DetachFromGroupOwnerRecursive();
51   mMenu->ClearObserver();
52   mMenu->ClearIconListener();
55 static void UpdateMenu(nsMenuX* aMenu) {
56   aMenu->MenuOpened();
57   aMenu->MenuClosed();
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>>());
64     }
65   }
68 void NativeMenuMac::MenuWillOpen() {
69   // Force an update on the mMenu by faking an open/close on all of
70   // its submenus.
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*>(
83                                          aIndexString.BeginReading())
84                               length:aIndexString.Length()];
85   NSMenuItem* item =
86       nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
88   // We can't perform an action on an item with a submenu, that will raise
89   // an obj-c exception.
90   if (item && !item.hasSubmenu) {
91     NSMenu* parent = item.menu;
92     if (parent) {
93       // NSLog(@"Performing action for native menu item titled: %@\n",
94       //       [[currentSubmenu itemAtIndex:targetIndex] title]);
95       mozilla::AutoRestore<bool> autoRestore(
96           nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
97       nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
98       [parent performActionForItemAtIndex:[parent indexOfItem:item]];
99       return true;
100     }
101   }
103   return false;
105   NS_OBJC_END_TRY_ABORT_BLOCK;
108 void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
109   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
111   NSString* locationString =
112       [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
113                                          aIndexString.BeginReading())
114                               length:aIndexString.Length()];
115   NSArray<NSString*>* indexes =
116       [locationString componentsSeparatedByString:@"|"];
117   RefPtr<nsMenuX> currentMenu = mMenu.get();
119   // now find the correct submenu
120   unsigned int indexCount = indexes.count;
121   for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
122     int targetIndex = [indexes objectAtIndex:i].intValue;
123     int visible = 0;
124     uint32_t length = currentMenu->GetItemCount();
125     for (unsigned int j = 0; j < length; j++) {
126       Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
127       if (!targetMenu) {
128         return;
129       }
130       RefPtr<nsIContent> content = targetMenu->match(
131           [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
132           [](const RefPtr<nsMenuItemX>& aMenuItem) {
133             return aMenuItem->Content();
134           });
135       if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
136         visible++;
137         if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
138           currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
139           break;
140         }
141       }
142     }
143   }
145   // fake open/close to cause lazy update to happen
146   currentMenu->MenuOpened();
147   currentMenu->MenuClosed();
149   NS_OBJC_END_TRY_ABORT_BLOCK;
152 void NativeMenuMac::IconUpdated() {
153   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
155   if (mContainerStatusBarItem) {
156     NSImage* menuImage = mMenu->NativeNSMenuItem().image;
157     if (menuImage) {
158       [menuImage setTemplate:YES];
159     }
160     mContainerStatusBarItem.button.image = menuImage;
161   }
163   NS_OBJC_END_TRY_ABORT_BLOCK;
166 void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
167   mContainerStatusBarItem = aItem;
168   IconUpdated();
171 void NativeMenuMac::Dump() {
172   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
174   mMenu->Dump(0);
175   nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
177   NS_OBJC_END_TRY_ABORT_BLOCK;
180 void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
181   if (aPopupElement == mElement) {
182     return;
183   }
185   // Our caller isn't keeping us alive, so make sure we stay alive throughout
186   // this function in case one of the observer notifications destroys us.
187   RefPtr<NativeMenuMac> kungFuDeathGrip(this);
189   for (NativeMenu::Observer* observer : mObservers.Clone()) {
190     observer->OnNativeSubMenuWillOpen(aPopupElement);
191   }
194 void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
195   // Our caller isn't keeping us alive, so make sure we stay alive throughout
196   // this function in case one of the observer notifications destroys us.
197   RefPtr<NativeMenuMac> kungFuDeathGrip(this);
199   for (NativeMenu::Observer* observer : mObservers.Clone()) {
200     if (aPopupElement == mElement) {
201       observer->OnNativeMenuOpened();
202     } else {
203       observer->OnNativeSubMenuDidOpen(aPopupElement);
204     }
205   }
208 void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
209                                            dom::Element* aMenuItemElement) {
210   // Our caller isn't keeping us alive, so make sure we stay alive throughout
211   // this function in case one of the observer notifications destroys us.
212   RefPtr<NativeMenuMac> kungFuDeathGrip(this);
214   for (NativeMenu::Observer* observer : mObservers.Clone()) {
215     observer->OnNativeMenuWillActivateItem(aMenuItemElement);
216   }
219 void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
220   // Our caller isn't keeping us alive, so make sure we stay alive throughout
221   // this function in case one of the observer notifications destroys us.
222   RefPtr<NativeMenuMac> kungFuDeathGrip(this);
224   for (NativeMenu::Observer* observer : mObservers.Clone()) {
225     if (aPopupElement == mElement) {
226       observer->OnNativeMenuClosed();
227     } else {
228       observer->OnNativeSubMenuClosed(aPopupElement);
229     }
230   }
233 static NSView* NativeViewForFrame(nsIFrame* aFrame) {
234   nsIWidget* widget = aFrame->GetNearestWidget();
235   return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
238 static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
239   nsIFrame* f = aContent->GetPrimaryFrame();
240   if (!f) {
241     return nil;
242   }
243   return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
246 void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame,
247                                       const CSSIntPoint& aPosition,
248                                       bool aIsContextMenu) {
249   nsPresContext* pc = aClickedFrame->PresContext();
250   auto cssToDesktopScale =
251       pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
252   const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
254   mMenu->PopupShowingEventWasSentAndApprovedExternally();
256   NSMenu* menu = mMenu->NativeNSMenu();
257   NSView* view = NativeViewForFrame(aClickedFrame);
258   NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
259   NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
261   // Let the MOZMenuOpeningCoordinator do the actual opening, so that this
262   // ShowAsContextMenu call does not spawn a nested event loop, which would be
263   // surprising to our callers.
264   mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance
265       asynchronouslyOpenMenu:menu
266             atScreenPosition:locationOnScreen
267                      forView:view
268               withAppearance:appearance
269                asContextMenu:aIsContextMenu];
272 bool NativeMenuMac::Close() {
273   if (mOpeningHandle) {
274     // In case the menu was trying to open, but this Close() call interrupted
275     // it, cancel opening.
276     [MOZMenuOpeningCoordinator.sharedInstance
277         cancelAsynchronousOpening:mOpeningHandle];
278   }
279   return mMenu->Close();
282 RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(
283     dom::Element* aElement) {
284   nsTArray<RefPtr<dom::Element>> submenuChain;
285   RefPtr<dom::Element> currentElement = aElement->GetParentElement();
286   while (currentElement && currentElement != mElement) {
287     if (currentElement->IsXULElement(nsGkAtoms::menu)) {
288       submenuChain.AppendElement(currentElement);
289     }
290     currentElement = currentElement->GetParentElement();
291   }
292   if (!currentElement) {
293     // aElement was not a descendent of mElement. Refuse to activate the item.
294     return nullptr;
295   }
297   // Traverse submenuChain from shallow to deep, to find the nsMenuX that
298   // contains aElement.
299   submenuChain.Reverse();
300   RefPtr<nsMenuX> menu = mMenu;
301   for (const auto& submenu : submenuChain) {
302     if (!menu->IsOpenForGecko()) {
303       // Refuse to descend into closed menus.
304       return nullptr;
305     }
306     Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
307     if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
308       // Couldn't find submenu.
309       return nullptr;
310     }
311     menu = menuChild->as<RefPtr<nsMenuX>>();
312   }
314   if (!menu->IsOpenForGecko()) {
315     // Refuse to descend into closed menus.
316     return nullptr;
317   }
318   return menu;
321 static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
322   NSEventModifierFlags flags = 0;
323   if (aModifiers & MODIFIER_CONTROL) {
324     flags |= NSEventModifierFlagControl;
325   }
326   if (aModifiers & MODIFIER_ALT) {
327     flags |= NSEventModifierFlagOption;
328   }
329   if (aModifiers & MODIFIER_SHIFT) {
330     flags |= NSEventModifierFlagShift;
331   }
332   if (aModifiers & MODIFIER_META) {
333     flags |= NSEventModifierFlagCommand;
334   }
335   return flags;
338 void NativeMenuMac::ActivateItem(dom::Element* aItemElement,
339                                  Modifiers aModifiers, int16_t aButton,
340                                  ErrorResult& aRv) {
341   RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
342   if (!menu) {
343     aRv.ThrowInvalidStateError("Menu containing menu item is not open");
344     return;
345   }
346   Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
347   if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
348     aRv.ThrowInvalidStateError("Could not find the supplied menu item");
349     return;
350   }
352   RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
353   if (!item->IsVisible()) {
354     aRv.ThrowInvalidStateError("Menu item is not visible");
355     return;
356   }
358   NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
360   // First, initiate the closing of the NSMenu.
361   // This synchronously calls the menu delegate's menuDidClose handler. So
362   // menuDidClose is what runs first; this matches the order of events for
363   // user-initiated menu item activation. This call doesn't immediately hide the
364   // menu; the menu only hides once the stack unwinds from NSMenu's nested
365   // "tracking" event loop.
366   [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
368   // Next, call OnWillActivateItem. This also matches the order of calls that
369   // happen when a user activates a menu item in the real world: -[MenuDelegate
370   // menu:willActivateItem:] runs after menuDidClose.
371   menu->OnWillActivateItem(nativeItem);
373   // Finally, call ActivateItemAfterClosing. This also mimics the order in the
374   // real world: menuItemHit is called after menu:willActivateItem:.
375   menu->ActivateItemAfterClosing(std::move(item),
376                                  ConvertModifierFlags(aModifiers), aButton);
378   // Tell our native event loop that it should not process any more work before
379   // unwinding the stack, so that we can get out of the menu's nested event loop
380   // as fast as possible. This was needed to fix spurious failures in tests,
381   // where a call to cancelTrackingWithoutAnimation was ignored if more native
382   // events were processed before the event loop was exited. As a result, the
383   // menu stayed open forever and the test never finished.
384   MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
386   [nativeItem release];
389 void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
390   if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
391     Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
392     if (item && item->is<RefPtr<nsMenuX>>()) {
393       item->as<RefPtr<nsMenuX>>()->MenuOpened();
394     }
395   }
398 void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
399   if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
400     Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
401     if (item && item->is<RefPtr<nsMenuX>>()) {
402       item->as<RefPtr<nsMenuX>>()->MenuClosed();
403     }
404   }
407 RefPtr<Element> NativeMenuMac::Element() { return mElement; }
409 }  // namespace widget
410 }  // namespace mozilla