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"
38 NativeMenuMac::NativeMenuMac(dom::Element* aElement)
39 : mElement(aElement), mContainerStatusBarItem(nil) {
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);
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*>(
83 aIndexString.BeginReading())
84 length:aIndexString.Length()];
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;
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]];
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;
124 uint32_t length = currentMenu->GetItemCount();
125 for (unsigned int j = 0; j < length; j++) {
126 Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
130 RefPtr<nsIContent> content = targetMenu->match(
131 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
132 [](const RefPtr<nsMenuItemX>& aMenuItem) {
133 return aMenuItem->Content();
135 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
137 if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
138 currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
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;
158 [menuImage setTemplate:YES];
160 mContainerStatusBarItem.button.image = menuImage;
163 NS_OBJC_END_TRY_ABORT_BLOCK;
166 void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
167 mContainerStatusBarItem = aItem;
171 void NativeMenuMac::Dump() {
172 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
175 nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
177 NS_OBJC_END_TRY_ABORT_BLOCK;
180 void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
181 if (aPopupElement == mElement) {
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);
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();
203 observer->OnNativeSubMenuDidOpen(aPopupElement);
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);
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();
228 observer->OnNativeSubMenuClosed(aPopupElement);
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();
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
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];
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);
290 currentElement = currentElement->GetParentElement();
292 if (!currentElement) {
293 // aElement was not a descendent of mElement. Refuse to activate the item.
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.
306 Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
307 if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
308 // Couldn't find submenu.
311 menu = menuChild->as<RefPtr<nsMenuX>>();
314 if (!menu->IsOpenForGecko()) {
315 // Refuse to descend into closed menus.
321 static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
322 NSEventModifierFlags flags = 0;
323 if (aModifiers & MODIFIER_CONTROL) {
324 flags |= NSEventModifierFlagControl;
326 if (aModifiers & MODIFIER_ALT) {
327 flags |= NSEventModifierFlagOption;
329 if (aModifiers & MODIFIER_SHIFT) {
330 flags |= NSEventModifierFlagShift;
332 if (aModifiers & MODIFIER_META) {
333 flags |= NSEventModifierFlagCommand;
338 void NativeMenuMac::ActivateItem(dom::Element* aItemElement,
339 Modifiers aModifiers, int16_t aButton,
341 RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
343 aRv.ThrowInvalidStateError("Menu containing menu item is not open");
347 nsMenuUtilsX::CheckNativeMenuConsistency(menu->NativeNSMenu());
349 Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
350 if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
351 aRv.ThrowInvalidStateError("Could not find the supplied menu item");
355 RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
356 if (!item->IsVisible()) {
357 aRv.ThrowInvalidStateError("Menu item is not visible");
361 NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
363 // First, initiate the closing of the NSMenu.
364 // This synchronously calls the menu delegate's menuDidClose handler. So
365 // menuDidClose is what runs first; this matches the order of events for
366 // user-initiated menu item activation. This call doesn't immediately hide the
367 // menu; the menu only hides once the stack unwinds from NSMenu's nested
368 // "tracking" event loop.
369 [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
371 // Next, call OnWillActivateItem. This also matches the order of calls that
372 // happen when a user activates a menu item in the real world: -[MenuDelegate
373 // menu:willActivateItem:] runs after menuDidClose.
374 menu->OnWillActivateItem(nativeItem);
376 // Finally, call ActivateItemAfterClosing. This also mimics the order in the
377 // real world: menuItemHit is called after menu:willActivateItem:.
378 menu->ActivateItemAfterClosing(std::move(item),
379 ConvertModifierFlags(aModifiers), aButton);
381 // Tell our native event loop that it should not process any more work before
382 // unwinding the stack, so that we can get out of the menu's nested event loop
383 // as fast as possible. This was needed to fix spurious failures in tests,
384 // where a call to cancelTrackingWithoutAnimation was ignored if more native
385 // events were processed before the event loop was exited. As a result, the
386 // menu stayed open forever and the test never finished.
387 MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
389 [nativeItem release];
392 void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
393 if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
394 Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
395 if (item && item->is<RefPtr<nsMenuX>>()) {
396 item->as<RefPtr<nsMenuX>>()->MenuOpened();
401 void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
402 if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
403 Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
404 if (item && item->is<RefPtr<nsMenuX>>()) {
405 item->as<RefPtr<nsMenuX>>()->MenuClosed();
410 RefPtr<Element> NativeMenuMac::Element() { return mElement; }
412 } // namespace widget
413 } // namespace mozilla