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 #include "nsMenuBarFrame.h"
7 #include "nsIServiceManager.h"
8 #include "nsIContent.h"
9 #include "nsIAtom.h"
10 #include "nsPresContext.h"
11 #include "nsStyleContext.h"
12 #include "nsCSSRendering.h"
13 #include "nsNameSpaceManager.h"
14 #include "nsIDocument.h"
15 #include "nsGkAtoms.h"
16 #include "nsMenuFrame.h"
17 #include "nsMenuPopupFrame.h"
18 #include "nsUnicharUtils.h"
19 #include "nsPIDOMWindow.h"
20 #include "nsIInterfaceRequestorUtils.h"
21 #include "nsCSSFrameConstructor.h"
22 #ifdef XP_WIN
23 #include "nsISound.h"
24 #include "nsWidgetsCID.h"
25 #endif
26 #include "nsContentUtils.h"
27 #include "nsUTF8Utils.h"
28 #include "mozilla/TextEvents.h"
29 #include "mozilla/dom/Event.h"
30 
31 using namespace mozilla;
32 
33 //
34 // NS_NewMenuBarFrame
35 //
36 // Wrapper for creating a new menu Bar container
37 //
38 nsIFrame*
NS_NewMenuBarFrame(nsIPresShell * aPresShell,nsStyleContext * aContext)39 NS_NewMenuBarFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
40 {
41   return new (aPresShell) nsMenuBarFrame(aContext);
42 }
43 
44 NS_IMPL_FRAMEARENA_HELPERS(nsMenuBarFrame)
45 
NS_QUERYFRAME_HEAD(nsMenuBarFrame)46 NS_QUERYFRAME_HEAD(nsMenuBarFrame)
47   NS_QUERYFRAME_ENTRY(nsMenuBarFrame)
48 NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
49 
50 //
51 // nsMenuBarFrame cntr
52 //
53 nsMenuBarFrame::nsMenuBarFrame(nsStyleContext* aContext):
54   nsBoxFrame(aContext),
55     mStayActive(false),
56     mIsActive(false),
57     mCurrentMenu(nullptr),
58     mTarget(nullptr)
59 {
60 } // cntr
61 
62 void
Init(nsIContent * aContent,nsContainerFrame * aParent,nsIFrame * aPrevInFlow)63 nsMenuBarFrame::Init(nsIContent*       aContent,
64                      nsContainerFrame* aParent,
65                      nsIFrame*         aPrevInFlow)
66 {
67   nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
68 
69   // Create the menu bar listener.
70   mMenuBarListener = new nsMenuBarListener(this);
71 
72   // Hook up the menu bar as a key listener on the whole document.  It will see every
73   // key press that occurs, but after everyone else does.
74   mTarget = aContent->GetComposedDoc();
75 
76   // Also hook up the listener to the window listening for focus events. This is so we can keep proper
77   // state as the user alt-tabs through processes.
78 
79   mTarget->AddSystemEventListener(NS_LITERAL_STRING("keypress"), mMenuBarListener, false);
80   mTarget->AddSystemEventListener(NS_LITERAL_STRING("keydown"), mMenuBarListener, false);
81   mTarget->AddSystemEventListener(NS_LITERAL_STRING("keyup"), mMenuBarListener, false);
82   mTarget->AddSystemEventListener(NS_LITERAL_STRING("mozaccesskeynotfound"), mMenuBarListener, false);
83 
84   // mousedown event should be handled in all phase
85   mTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), mMenuBarListener, true);
86   mTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), mMenuBarListener, false);
87   mTarget->AddEventListener(NS_LITERAL_STRING("blur"), mMenuBarListener, true);
88 
89   mTarget->AddEventListener(NS_LITERAL_STRING("MozDOMFullscreen:Entered"), mMenuBarListener, false);
90 }
91 
92 NS_IMETHODIMP
SetActive(bool aActiveFlag)93 nsMenuBarFrame::SetActive(bool aActiveFlag)
94 {
95   // If the activity is not changed, there is nothing to do.
96   if (mIsActive == aActiveFlag)
97     return NS_OK;
98 
99   if (!aActiveFlag) {
100     // Don't deactivate when switching between menus on the menubar.
101     if (mStayActive)
102       return NS_OK;
103 
104     // if there is a request to deactivate the menu bar, check to see whether
105     // there is a menu popup open for the menu bar. In this case, don't
106     // deactivate the menu bar.
107     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
108     if (pm && pm->IsPopupOpenForMenuParent(this))
109       return NS_OK;
110   }
111 
112   mIsActive = aActiveFlag;
113   if (mIsActive) {
114     InstallKeyboardNavigator();
115   }
116   else {
117     mActiveByKeyboard = false;
118     RemoveKeyboardNavigator();
119   }
120 
121   NS_NAMED_LITERAL_STRING(active, "DOMMenuBarActive");
122   NS_NAMED_LITERAL_STRING(inactive, "DOMMenuBarInactive");
123 
124   FireDOMEvent(mIsActive ? active : inactive, mContent);
125 
126   return NS_OK;
127 }
128 
129 nsMenuFrame*
ToggleMenuActiveState()130 nsMenuBarFrame::ToggleMenuActiveState()
131 {
132   if (mIsActive) {
133     // Deactivate the menu bar
134     SetActive(false);
135     if (mCurrentMenu) {
136       nsMenuFrame* closeframe = mCurrentMenu;
137       closeframe->SelectMenu(false);
138       mCurrentMenu = nullptr;
139       return closeframe;
140     }
141   }
142   else {
143     // if the menu bar is already selected (eg. mouseover), deselect it
144     if (mCurrentMenu)
145       mCurrentMenu->SelectMenu(false);
146 
147     // Set the active menu to be the top left item (e.g., the File menu).
148     // We use an attribute called "menuactive" to track the current
149     // active menu.
150     nsMenuFrame* firstFrame = nsXULPopupManager::GetNextMenuItem(this, nullptr, false);
151     if (firstFrame) {
152       // Activate the menu bar
153       SetActive(true);
154       firstFrame->SelectMenu(true);
155 
156       // Track this item for keyboard navigation.
157       mCurrentMenu = firstFrame;
158     }
159   }
160 
161   return nullptr;
162 }
163 
164 nsMenuFrame*
FindMenuWithShortcut(nsIDOMKeyEvent * aKeyEvent)165 nsMenuBarFrame::FindMenuWithShortcut(nsIDOMKeyEvent* aKeyEvent)
166 {
167   uint32_t charCode;
168   aKeyEvent->GetCharCode(&charCode);
169 
170   AutoTArray<uint32_t, 10> accessKeys;
171   WidgetKeyboardEvent* nativeKeyEvent =
172     aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
173   if (nativeKeyEvent) {
174     nativeKeyEvent->GetAccessKeyCandidates(accessKeys);
175   }
176   if (accessKeys.IsEmpty() && charCode)
177     accessKeys.AppendElement(charCode);
178 
179   if (accessKeys.IsEmpty())
180     return nullptr; // no character was pressed so just return
181 
182   // Enumerate over our list of frames.
183   auto insertion = PresContext()->PresShell()->FrameConstructor()->
184     GetInsertionPoint(GetContent(), nullptr);
185   nsContainerFrame* immediateParent = insertion.mParentFrame;
186   if (!immediateParent)
187     immediateParent = this;
188 
189   // Find a most preferred accesskey which should be returned.
190   nsIFrame* foundMenu = nullptr;
191   size_t foundIndex = accessKeys.NoIndex;
192   nsIFrame* currFrame = immediateParent->PrincipalChildList().FirstChild();
193 
194   while (currFrame) {
195     nsIContent* current = currFrame->GetContent();
196 
197     // See if it's a menu item.
198     if (nsXULPopupManager::IsValidMenuItem(current, false)) {
199       // Get the shortcut attribute.
200       nsAutoString shortcutKey;
201       current->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, shortcutKey);
202       if (!shortcutKey.IsEmpty()) {
203         ToLowerCase(shortcutKey);
204         const char16_t* start = shortcutKey.BeginReading();
205         const char16_t* end = shortcutKey.EndReading();
206         uint32_t ch = UTF16CharEnumerator::NextChar(&start, end);
207         size_t index = accessKeys.IndexOf(ch);
208         if (index != accessKeys.NoIndex &&
209             (foundIndex == accessKeys.NoIndex || index < foundIndex)) {
210           foundMenu = currFrame;
211           foundIndex = index;
212         }
213       }
214     }
215     currFrame = currFrame->GetNextSibling();
216   }
217   if (foundMenu) {
218     return do_QueryFrame(foundMenu);
219   }
220 
221   // didn't find a matching menu item
222 #ifdef XP_WIN
223   // behavior on Windows - this item is on the menu bar, beep and deactivate the menu bar
224   if (mIsActive) {
225     nsCOMPtr<nsISound> soundInterface = do_CreateInstance("@mozilla.org/sound;1");
226     if (soundInterface)
227       soundInterface->Beep();
228   }
229 
230   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
231   if (pm) {
232     nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny);
233     if (popup)
234       pm->HidePopup(popup->GetContent(), true, true, true, false);
235   }
236 
237   SetCurrentMenuItem(nullptr);
238   SetActive(false);
239 
240 #endif  // #ifdef XP_WIN
241 
242   return nullptr;
243 }
244 
245 /* virtual */ nsMenuFrame*
GetCurrentMenuItem()246 nsMenuBarFrame::GetCurrentMenuItem()
247 {
248   return mCurrentMenu;
249 }
250 
251 NS_IMETHODIMP
SetCurrentMenuItem(nsMenuFrame * aMenuItem)252 nsMenuBarFrame::SetCurrentMenuItem(nsMenuFrame* aMenuItem)
253 {
254   if (mCurrentMenu == aMenuItem)
255     return NS_OK;
256 
257   if (mCurrentMenu)
258     mCurrentMenu->SelectMenu(false);
259 
260   if (aMenuItem)
261     aMenuItem->SelectMenu(true);
262 
263   mCurrentMenu = aMenuItem;
264 
265   return NS_OK;
266 }
267 
268 void
CurrentMenuIsBeingDestroyed()269 nsMenuBarFrame::CurrentMenuIsBeingDestroyed()
270 {
271   mCurrentMenu->SelectMenu(false);
272   mCurrentMenu = nullptr;
273 }
274 
275 class nsMenuBarSwitchMenu : public Runnable
276 {
277 public:
nsMenuBarSwitchMenu(nsIContent * aMenuBar,nsIContent * aOldMenu,nsIContent * aNewMenu,bool aSelectFirstItem)278   nsMenuBarSwitchMenu(nsIContent* aMenuBar,
279                       nsIContent *aOldMenu,
280                       nsIContent *aNewMenu,
281                       bool aSelectFirstItem)
282     : mMenuBar(aMenuBar), mOldMenu(aOldMenu), mNewMenu(aNewMenu),
283       mSelectFirstItem(aSelectFirstItem)
284   {
285   }
286 
Run()287   NS_IMETHOD Run() override
288   {
289     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
290     if (!pm)
291       return NS_ERROR_UNEXPECTED;
292 
293     // if switching from one menu to another, set a flag so that the call to
294     // HidePopup doesn't deactivate the menubar when the first menu closes.
295     nsMenuBarFrame* menubar = nullptr;
296     if (mOldMenu && mNewMenu) {
297       menubar = do_QueryFrame(mMenuBar->GetPrimaryFrame());
298       if (menubar)
299         menubar->SetStayActive(true);
300     }
301 
302     if (mOldMenu) {
303       nsWeakFrame weakMenuBar(menubar);
304       pm->HidePopup(mOldMenu, false, false, false, false);
305       // clear the flag again
306       if (mNewMenu && weakMenuBar.IsAlive())
307         menubar->SetStayActive(false);
308     }
309 
310     if (mNewMenu)
311       pm->ShowMenu(mNewMenu, mSelectFirstItem, false);
312 
313     return NS_OK;
314   }
315 
316 private:
317   nsCOMPtr<nsIContent> mMenuBar;
318   nsCOMPtr<nsIContent> mOldMenu;
319   nsCOMPtr<nsIContent> mNewMenu;
320   bool mSelectFirstItem;
321 };
322 
323 NS_IMETHODIMP
ChangeMenuItem(nsMenuFrame * aMenuItem,bool aSelectFirstItem,bool aFromKey)324 nsMenuBarFrame::ChangeMenuItem(nsMenuFrame* aMenuItem,
325                                bool aSelectFirstItem,
326                                bool aFromKey)
327 {
328   if (mCurrentMenu == aMenuItem)
329     return NS_OK;
330 
331   // check if there's an open context menu, we ignore this
332   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
333   if (pm && pm->HasContextMenu(nullptr))
334     return NS_OK;
335 
336   nsIContent* aOldMenu = nullptr;
337   nsIContent* aNewMenu = nullptr;
338 
339   // Unset the current child.
340   bool wasOpen = false;
341   if (mCurrentMenu) {
342     wasOpen = mCurrentMenu->IsOpen();
343     mCurrentMenu->SelectMenu(false);
344     if (wasOpen) {
345       nsMenuPopupFrame* popupFrame = mCurrentMenu->GetPopup();
346       if (popupFrame)
347         aOldMenu = popupFrame->GetContent();
348     }
349   }
350 
351   // set to null first in case the IsAlive check below returns false
352   mCurrentMenu = nullptr;
353 
354   // Set the new child.
355   if (aMenuItem) {
356     nsCOMPtr<nsIContent> content = aMenuItem->GetContent();
357     aMenuItem->SelectMenu(true);
358     mCurrentMenu = aMenuItem;
359     if (wasOpen && !aMenuItem->IsDisabled())
360       aNewMenu = content;
361   }
362 
363   // use an event so that hiding and showing can be done synchronously, which
364   // avoids flickering
365   nsCOMPtr<nsIRunnable> event =
366     new nsMenuBarSwitchMenu(GetContent(), aOldMenu, aNewMenu, aSelectFirstItem);
367   return NS_DispatchToCurrentThread(event);
368 }
369 
370 nsMenuFrame*
Enter(WidgetGUIEvent * aEvent)371 nsMenuBarFrame::Enter(WidgetGUIEvent* aEvent)
372 {
373   if (!mCurrentMenu)
374     return nullptr;
375 
376   if (mCurrentMenu->IsOpen())
377     return mCurrentMenu->Enter(aEvent);
378 
379   return mCurrentMenu;
380 }
381 
382 bool
MenuClosed()383 nsMenuBarFrame::MenuClosed()
384 {
385   SetActive(false);
386   if (!mIsActive && mCurrentMenu) {
387     mCurrentMenu->SelectMenu(false);
388     mCurrentMenu = nullptr;
389     return true;
390   }
391   return false;
392 }
393 
394 void
InstallKeyboardNavigator()395 nsMenuBarFrame::InstallKeyboardNavigator()
396 {
397   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
398   if (pm)
399     pm->SetActiveMenuBar(this, true);
400 }
401 
402 void
RemoveKeyboardNavigator()403 nsMenuBarFrame::RemoveKeyboardNavigator()
404 {
405   if (!mIsActive) {
406     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
407     if (pm)
408       pm->SetActiveMenuBar(this, false);
409   }
410 }
411 
412 void
DestroyFrom(nsIFrame * aDestructRoot)413 nsMenuBarFrame::DestroyFrom(nsIFrame* aDestructRoot)
414 {
415   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
416   if (pm)
417     pm->SetActiveMenuBar(this, false);
418 
419   mTarget->RemoveSystemEventListener(NS_LITERAL_STRING("keypress"), mMenuBarListener, false);
420   mTarget->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"), mMenuBarListener, false);
421   mTarget->RemoveSystemEventListener(NS_LITERAL_STRING("keyup"), mMenuBarListener, false);
422   mTarget->RemoveSystemEventListener(NS_LITERAL_STRING("mozaccesskeynotfound"), mMenuBarListener, false);
423 
424   mTarget->RemoveEventListener(NS_LITERAL_STRING("mousedown"), mMenuBarListener, true);
425   mTarget->RemoveEventListener(NS_LITERAL_STRING("mousedown"), mMenuBarListener, false);
426   mTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), mMenuBarListener, true);
427 
428   mTarget->RemoveEventListener(NS_LITERAL_STRING("MozDOMFullscreen:Entered"), mMenuBarListener, false);
429 
430   mMenuBarListener->OnDestroyMenuBarFrame();
431   mMenuBarListener = nullptr;
432 
433   nsBoxFrame::DestroyFrom(aDestructRoot);
434 }
435