Merge mozilla-central to autoland on a CLOSED TREE
[gecko.git] / widget / cocoa / NativeMenuMac.mm
blob061d6c7ad97e19b790939564b6870da764df4f29
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"
31 #include "nsCocoaFeatures.h"
33 namespace mozilla {
35 using dom::Element;
37 namespace widget {
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);
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*>(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;
90     if (parent) {
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]];
97       return true;
98     }
99   }
101   return false;
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;
119     int visible = 0;
120     uint32_t length = currentMenu->GetItemCount();
121     for (unsigned int j = 0; j < length; j++) {
122       Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
123       if (!targetMenu) {
124         return;
125       }
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)) {
130         visible++;
131         if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
132           currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
133           break;
134         }
135       }
136     }
137   }
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;
151     if (menuImage) {
152       [menuImage setTemplate:YES];
153     }
154     mContainerStatusBarItem.button.image = menuImage;
155   }
157   NS_OBJC_END_TRY_ABORT_BLOCK;
160 void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
161   mContainerStatusBarItem = aItem;
162   IconUpdated();
165 void NativeMenuMac::Dump() {
166   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
168   mMenu->Dump(0);
169   nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
171   NS_OBJC_END_TRY_ABORT_BLOCK;
174 void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
175   if (aPopupElement == mElement) {
176     return;
177   }
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);
185   }
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();
196     } else {
197       observer->OnNativeSubMenuDidOpen(aPopupElement);
198     }
199   }
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);
210   }
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();
221     } else {
222       observer->OnNativeSubMenuClosed(aPopupElement);
223     }
224   }
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();
234   if (!f) {
235     return nil;
236   }
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
258                                                                             forView:view
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];
267   }
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);
277     }
278     currentElement = currentElement->GetParentElement();
279   }
280   if (!currentElement) {
281     // aElement was not a descendent of mElement. Refuse to activate the item.
282     return nullptr;
283   }
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.
291       return nullptr;
292     }
293     Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
294     if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
295       // Couldn't find submenu.
296       return nullptr;
297     }
298     menu = menuChild->as<RefPtr<nsMenuX>>();
299   }
301   if (!menu->IsOpenForGecko()) {
302     // Refuse to descend into closed menus.
303     return nullptr;
304   }
305   return menu;
308 static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
309   NSEventModifierFlags flags = 0;
310   if (aModifiers & MODIFIER_CONTROL) {
311     flags |= NSEventModifierFlagControl;
312   }
313   if (aModifiers & MODIFIER_ALT) {
314     flags |= NSEventModifierFlagOption;
315   }
316   if (aModifiers & MODIFIER_SHIFT) {
317     flags |= NSEventModifierFlagShift;
318   }
319   if (aModifiers & MODIFIER_META) {
320     flags |= NSEventModifierFlagCommand;
321   }
322   return flags;
325 void NativeMenuMac::ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, int16_t aButton,
326                                  ErrorResult& aRv) {
327   RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
328   if (!menu) {
329     aRv.ThrowInvalidStateError("Menu containing menu item is not open");
330     return;
331   }
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");
335     return;
336   }
338   RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
339   if (!item->IsVisible()) {
340     aRv.ThrowInvalidStateError("Menu item is not visible");
341     return;
342   }
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
355   // menuDidClose.
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();
378     }
379   }
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();
387     }
388   }
391 RefPtr<Element> NativeMenuMac::Element() { return mElement; }
393 }  // namespace widget
394 }  // namespace mozilla