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