1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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/. */
5 
6 #ifndef nsMenuX_h_
7 #define nsMenuX_h_
8 
9 #import <Cocoa/Cocoa.h>
10 
11 #include "mozilla/EventForwards.h"
12 #include "mozilla/RefPtr.h"
13 #include "mozilla/UniquePtr.h"
14 #include "mozilla/Variant.h"
15 #include "nsISupports.h"
16 #include "nsMenuParentX.h"
17 #include "nsMenuBarX.h"
18 #include "nsMenuGroupOwnerX.h"
19 #include "nsMenuItemIconX.h"
20 #include "nsCOMPtr.h"
21 #include "nsChangeObserver.h"
22 #include "nsThreadUtils.h"
23 
24 class nsMenuX;
25 class nsMenuItemX;
26 class nsIWidget;
27 
28 // MenuDelegate is used to receive Cocoa notifications for setting
29 // up carbon events. Protocol is defined as of 10.6 SDK.
30 @interface MenuDelegate : NSObject <NSMenuDelegate> {
31   nsMenuX* mGeckoMenu;  // weak ref
32   NSMutableArray* mBlocksToRunWhenOpen;
33 }
34 - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu;
35 - (void)runBlockWhenOpen:(void (^)())block;
36 - (void)menu:(NSMenu*)menu willActivateItem:(NSMenuItem*)item;
37 @property BOOL menuIsInMenubar;
38 @end
39 
40 class nsMenuXObserver {
41  public:
42   // Called when a menu in this menu subtree opens, before popupshowing.
43   // No strong reference is held to the observer during the call.
44   virtual void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) = 0;
45 
46   // Called when a menu in this menu subtree opened, after popupshown.
47   // No strong reference is held to the observer during the call.
48   virtual void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) = 0;
49 
50   // Called before a menu item is activated.
51   virtual void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
52                                       mozilla::dom::Element* aMenuItemElement) = 0;
53 
54   // Called when a menu in this menu subtree closed, after popuphidden.
55   // No strong reference is held to the observer during the call.
56   virtual void OnMenuClosed(mozilla::dom::Element* aPopupElement) = 0;
57 };
58 
59 // Once instantiated, this object lives until its DOM node or its parent window is destroyed.
60 // Do not hold references to this, they can become invalid any time the DOM node can be destroyed.
61 class nsMenuX final : public nsMenuParentX,
62                       public nsChangeObserver,
63                       public nsMenuItemIconX::Listener,
64                       public nsMenuXObserver {
65  public:
66   using Observer = nsMenuXObserver;
67 
68   // aParent is optional.
69   nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent);
70 
71   NS_INLINE_DECL_REFCOUNTING(nsMenuX)
72 
73   // If > 0, the OS is indexing all the app's menus (triggered by opening
74   // Help menu on Leopard and higher).  There are some things that are
75   // unsafe to do while this is happening.
76   static int32_t sIndexingMenuLevel;
77 
78   NS_DECL_CHANGEOBSERVER
79 
80   // nsMenuItemIconX::Listener
81   void IconUpdated() override;
82 
83   // nsMenuXObserver, to forward notifications from our children to our observer.
84   void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
85   void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
86   void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
87                               mozilla::dom::Element* aMenuItemElement) override;
88   void OnMenuClosed(mozilla::dom::Element* aPopupElement) override;
89 
IsVisible()90   bool IsVisible() const { return mVisible; }
91 
92   // Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group owner pointer, on this
93   // nsMenuX and also all nested nsMenuX and nsMenuItemX objects.
94   // This is needed because nsMenuX is reference-counted and can outlive its owner, and the menu
95   // group owner asserts that everything has been unregistered when it is destroyed.
96   void DetachFromGroupOwnerRecursive();
97 
98   // Nulls out our reference to the parent.
99   // This is needed because nsMenuX is reference-counted and can outlive its parent.
DetachFromParent()100   void DetachFromParent() { mParent = nullptr; }
101 
102   mozilla::Maybe<MenuChild> GetItemAt(uint32_t aPos);
103   uint32_t GetItemCount();
104 
105   mozilla::Maybe<MenuChild> GetVisibleItemAt(uint32_t aPos);
106   nsresult GetVisibleItemCount(uint32_t& aCount);
107 
108   mozilla::Maybe<MenuChild> GetItemForElement(mozilla::dom::Element* aMenuChildElement);
109 
110   // Asynchronously runs the command event on aItem, after the root menu has closed.
111   void ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
112                                 int16_t aButton);
113 
IsOpenForGecko()114   bool IsOpenForGecko() const { return mIsOpenForGecko; }
115 
116   // Fires the popupshowing event and returns whether the handler allows the popup to open.
117   // When calling this method, the caller must hold a strong reference to this object, because other
118   // references to this object can be dropped during the handling of the DOM event.
119   bool OnOpen();
120 
PopupShowingEventWasSentAndApprovedExternally()121   void PopupShowingEventWasSentAndApprovedExternally() { DidFirePopupShowing(); }
122 
123   // Called from the menu delegate during menuWillOpen, or to simulate opening.
124   // Ignored if the menu is already considered open.
125   // When calling this method, the caller must hold a strong reference to this object, because other
126   // references to this object can be dropped during the handling of the DOM event.
127   void MenuOpened();
128 
129   // Called from the menu delegate during menuDidClose, or to simulate closing.
130   // Ignored if the menu is already considered closed.
131   // When calling this method, the caller must hold a strong reference to this object, because other
132   // references to this object can be dropped during the handling of the DOM event.
133   // If aEntireMenuClosingDueToActivateItem is true, it means that popuphiding/popuphidden events
134   // can be delayed until the event loop for the menu is exited. If this is a submenu, this is
135   // usually not possible because the rest of the menu might stay open.
136   void MenuClosed(bool aEntireMenuClosingDueToActivateItem = false);
137 
138   // Close the menu if it's open, and flush any pending popuphiding / popuphidden events.
139   bool Close();
140 
141   // Called from the menu delegate during menu:willHighlightItem:.
142   // If called with Nothing(), it means that no item is highlighted.
143   // The index only accounts for visible items, i.e. items for which there exists an NSMenuItem* in
144   // mNativeMenu.
145   void OnHighlightedItemChanged(const mozilla::Maybe<uint32_t>& aNewHighlightedIndex);
146 
147   // Called from the menu delegate before an item anywhere in this menu is activated.
148   // Called after MenuClosed().
149   void OnWillActivateItem(NSMenuItem* aItem);
150 
151   void SetRebuild(bool aMenuEvent);
152   void SetupIcon();
Content()153   nsIContent* Content() { return mContent; }
NativeNSMenuItem()154   NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
NativeNSMenu()155   GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
156 
SetIconListener(nsMenuItemIconX::Listener * aListener)157   void SetIconListener(nsMenuItemIconX::Listener* aListener) { mIconListener = aListener; }
ClearIconListener()158   void ClearIconListener() { mIconListener = nullptr; }
159 
160   // nsMenuParentX
161   void MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) override;
162 
163   void Dump(uint32_t aIndent) const;
164 
165   static bool IsXULHelpMenu(nsIContent* aMenuContent);
166 
167   // Set an observer that gets notified of menu opening and closing.
168   // The menu does not keep a strong reference the observer. The observer must
169   // remove itself before it is destroyed.
SetObserver(Observer * aObserver)170   void SetObserver(Observer* aObserver) { mObserver = aObserver; }
171 
172   // Stop observing.
ClearObserver()173   void ClearObserver() { mObserver = nullptr; }
174 
175  protected:
176   virtual ~nsMenuX();
177 
178   void RebuildMenu();
179   nsresult RemoveAll();
180   nsresult SetEnabled(bool aIsEnabled);
181   nsresult GetEnabled(bool* aIsEnabled);
182   already_AddRefed<nsIContent> GetMenuPopupContent();
183   void WillInsertChild(const MenuChild& aChild);
184   void WillRemoveChild(const MenuChild& aChild);
185   void AddMenuChild(MenuChild&& aChild);
186   void InsertMenuChild(MenuChild&& aChild);
187   void RemoveMenuChild(const MenuChild& aChild);
188   mozilla::Maybe<MenuChild> CreateMenuChild(nsIContent* aContent);
189   RefPtr<nsMenuItemX> CreateMenuItem(nsIContent* aMenuItemContent);
190   GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle);
191   void DidFirePopupShowing();
192 
193   // Find the index at which aChild needs to be inserted into mMenuChildren such that mMenuChildren
194   // remains in correct content order, i.e. the order in mMenuChildren is the same as the order of
195   // the DOM children of our <menupopup>.
196   size_t FindInsertionIndex(const MenuChild& aChild);
197 
198   // Calculates the index at which aChild's NSMenuItem should be inserted into our NSMenu.
199   // The order of NSMenuItems in the NSMenu is the same as the order of menu children in
200   // mMenuChildren; the only difference is that mMenuChildren contains both visible and invisible
201   // children, and the NSMenu only contains visible items. So the insertion index is equal to the
202   // number of visible previous siblings of aChild in mMenuChildren.
203   NSInteger CalculateNativeInsertionPoint(const MenuChild& aChild);
204 
205   // Fires the popupshown event.
206   void MenuOpenedAsync();
207 
208   // Called from mPendingAsyncMenuCloseRunnable asynchronously after MenuClosed(), so that it runs
209   // after any potential menuItemHit calls for clicked menu items.
210   // Fires popuphiding and popuphidden events.
211   // When calling this method, the caller must hold a strong reference to this object, because other
212   // references to this object can be dropped during the handling of the DOM event.
213   void MenuClosedAsync();
214 
215   // If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to send out the pending
216   // popupshown event.
217   void FlushMenuOpenedRunnable();
218 
219   // If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to send out pending
220   // popuphiding/popuphidden events.
221   void FlushMenuClosedRunnable();
222 
223   // Make sure the NSMenu contains at least one item, even if mVisibleItemsCount is zero.
224   // Otherwise it won't open.
225   void InsertPlaceholderIfNeeded();
226   // Remove the placeholder before adding an item to mNativeNSMenu.
227   void RemovePlaceholderIfPresent();
228 
229   nsCOMPtr<nsIContent> mContent;  // XUL <menu> or <menupopup>
230 
231   // Contains nsMenuX and nsMenuItemX objects
232   nsTArray<MenuChild> mMenuChildren;
233 
234   nsString mLabel;
235   uint32_t mVisibleItemsCount = 0;                     // cache
236   nsMenuParentX* mParent = nullptr;                    // [weak]
237   nsMenuGroupOwnerX* mMenuGroupOwner = nullptr;        // [weak]
238   nsMenuItemIconX::Listener* mIconListener = nullptr;  // [weak]
239   mozilla::UniquePtr<nsMenuItemIconX> mIcon;
240 
241   Observer* mObserver = nullptr;  // non-owning pointer to our observer
242 
243   // Non-null between a call to MenuOpened() and MenuOpenedAsync().
244   RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuOpenRunnable;
245 
246   // Non-null between a call to MenuClosed() and MenuClosedAsync().
247   // This is asynchronous so that, if a menu item is clicked, we can fire popuphiding *after* we
248   // execute the menu item command. The macOS menu system calls menuWillClose *before* it calls
249   // menuItemHit.
250   RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuCloseRunnable;
251 
252   // Any runnables for running asynchronous command events.
253   // These are only used during automated tests, via ActivateItemAfterClosing.
254   // We keep track of them here so that we can ensure they're run before popuphiding/popuphidden.
255   nsTArray<RefPtr<mozilla::Runnable>> mPendingCommandRunnables;
256 
257   GeckoNSMenu* mNativeMenu = nil;     // [strong]
258   MenuDelegate* mMenuDelegate = nil;  // [strong]
259   // nsMenuX objects should always have a valid native menu item.
260   NSMenuItem* mNativeMenuItem = nil;  // [strong]
261 
262   // Nothing() if no item is highlighted. The index only accounts for visible items.
263   mozilla::Maybe<uint32_t> mHighlightedItemIndex;
264 
265   bool mIsEnabled = true;
266   bool mNeedsRebuild = true;
267 
268   // Whether the native NSMenu is considered open.
269   // Also affected by MenuOpened() / MenuClosed() calls for simulated opening / closing.
270   bool mIsOpen = false;
271 
272   // Whether the popup is open from Gecko's perspective, based on popupshowing / popuphiding events.
273   bool mIsOpenForGecko = false;
274 
275   bool mVisible = true;
276 
277   // true between an OnOpen() call that returned true, and the subsequent call
278   // to MenuOpened().
279   bool mDidFirePopupshowingAndIsApprovedToOpen = false;
280 };
281 
282 #endif  // nsMenuX_h_
283