Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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/. */
6 #include <objc/objc-runtime.h>
8 #include "nsMenuBarX.h"
9 #include "nsMenuX.h"
10 #include "nsMenuItemX.h"
11 #include "nsMenuUtilsX.h"
12 #include "nsCocoaFeatures.h"
13 #include "nsCocoaUtils.h"
14 #include "nsCocoaWindow.h"
15 #include "nsChildView.h"
17 #include "nsCOMPtr.h"
18 #include "nsString.h"
19 #include "nsGkAtoms.h"
20 #include "nsObjCExceptions.h"
21 #include "nsThreadUtils.h"
23 #include "nsIContent.h"
24 #include "nsIWidget.h"
25 #include "nsIDocument.h"
26 #include "nsIDOMDocument.h"
27 #include "nsIDOMElement.h"
29 NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
30 nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr; // Weak
31 nsMenuBarX* nsMenuBarX::sCurrentPaintDelayedMenuBar = nullptr; // Weak
32 NSMenu* sApplicationMenu = nil;
33 BOOL gSomeMenuBarPainted = NO;
35 // We keep references to the first quit and pref item content nodes we find, which
36 // will be from the hidden window. We use these when the document for the current
37 // window does not have a quit or pref item. We don't need strong refs here because
38 // these items are always strong ref'd by their owning menu bar (instance variable).
39 static nsIContent* sAboutItemContent = nullptr;
40 static nsIContent* sUpdateItemContent = nullptr;
41 static nsIContent* sPrefItemContent = nullptr;
42 static nsIContent* sQuitItemContent = nullptr;
44 NS_IMPL_ISUPPORTS(nsNativeMenuServiceX, nsINativeMenuService)
46 NS_IMETHODIMP nsNativeMenuServiceX::CreateNativeMenuBar(nsIWidget* aParent, nsIContent* aMenuBarNode)
47 {
48 NS_ASSERTION(NS_IsMainThread(), "Attempting to create native menu bar on wrong thread!");
50 nsRefPtr<nsMenuBarX> mb = new nsMenuBarX();
51 if (!mb)
52 return NS_ERROR_OUT_OF_MEMORY;
54 return mb->Create(aParent, aMenuBarNode);
55 }
57 nsMenuBarX::nsMenuBarX()
58 : nsMenuGroupOwnerX(), mParentWindow(nullptr), mAwaitingDelayedPaint(false)
59 {
60 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
62 mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar" andMenuBarOwner:this];
64 NS_OBJC_END_TRY_ABORT_BLOCK;
65 }
67 nsMenuBarX::~nsMenuBarX()
68 {
69 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
71 if (nsMenuBarX::sLastGeckoMenuBarPainted == this)
72 nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
74 // the quit/pref items of a random window might have been used if there was no
75 // hidden window, thus we need to invalidate the weak references.
76 if (sAboutItemContent == mAboutItemContent)
77 sAboutItemContent = nullptr;
78 if (sUpdateItemContent == mUpdateItemContent)
79 sUpdateItemContent = nullptr;
80 if (sQuitItemContent == mQuitItemContent)
81 sQuitItemContent = nullptr;
82 if (sPrefItemContent == mPrefItemContent)
83 sPrefItemContent = nullptr;
85 // make sure we unregister ourselves as a content observer
86 UnregisterForContentChanges(mContent);
88 // We have to manually clear the array here because clearing causes menu items
89 // to call back into the menu bar to unregister themselves. We don't want to
90 // depend on member variable ordering to ensure that the array gets cleared
91 // before the registration hash table is destroyed.
92 mMenuArray.Clear();
94 [mNativeMenu resetMenuBarOwner];
95 [mNativeMenu release];
97 NS_OBJC_END_TRY_ABORT_BLOCK;
98 }
100 nsresult nsMenuBarX::Create(nsIWidget* aParent, nsIContent* aContent)
101 {
102 if (!aParent || !aContent)
103 return NS_ERROR_INVALID_ARG;
105 mParentWindow = aParent;
106 mContent = aContent;
108 AquifyMenuBar();
110 nsresult rv = nsMenuGroupOwnerX::Create(aContent);
111 if (NS_FAILED(rv))
112 return rv;
114 RegisterForContentChanges(aContent, this);
116 ConstructNativeMenus();
118 // Give this to the parent window. The parent takes ownership.
119 static_cast<nsCocoaWindow*>(mParentWindow)->SetMenuBar(this);
121 return NS_OK;
122 }
124 void nsMenuBarX::ConstructNativeMenus()
125 {
126 uint32_t count = mContent->GetChildCount();
127 for (uint32_t i = 0; i < count; i++) {
128 nsIContent *menuContent = mContent->GetChildAt(i);
129 if (menuContent &&
130 menuContent->Tag() == nsGkAtoms::menu &&
131 menuContent->IsXUL()) {
132 nsMenuX* newMenu = new nsMenuX();
133 if (newMenu) {
134 nsresult rv = newMenu->Create(this, this, menuContent);
135 if (NS_SUCCEEDED(rv))
136 InsertMenuAtIndex(newMenu, GetMenuCount());
137 else
138 delete newMenu;
139 }
140 }
141 }
142 }
144 uint32_t nsMenuBarX::GetMenuCount()
145 {
146 return mMenuArray.Length();
147 }
149 bool nsMenuBarX::MenuContainsAppMenu()
150 {
151 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
153 return ([mNativeMenu numberOfItems] > 0 &&
154 [[mNativeMenu itemAtIndex:0] submenu] == sApplicationMenu);
156 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false);
157 }
159 nsresult nsMenuBarX::InsertMenuAtIndex(nsMenuX* aMenu, uint32_t aIndex)
160 {
161 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
163 // If we haven't created a global Application menu yet, do it.
164 if (!sApplicationMenu) {
165 nsresult rv = NS_OK; // avoid warning about rv being unused
166 rv = CreateApplicationMenu(aMenu);
167 NS_ASSERTION(NS_SUCCEEDED(rv), "Can't create Application menu");
169 // Hook the new Application menu up to the menu bar.
170 NSMenu* mainMenu = [NSApp mainMenu];
171 NS_ASSERTION([mainMenu numberOfItems] > 0, "Main menu does not have any items, something is terribly wrong!");
172 [[mainMenu itemAtIndex:0] setSubmenu:sApplicationMenu];
173 }
175 // add menu to array that owns our menus
176 mMenuArray.InsertElementAt(aIndex, aMenu);
178 // hook up submenus
179 nsIContent* menuContent = aMenu->Content();
180 if (menuContent->GetChildCount() > 0 &&
181 !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
182 int insertionIndex = nsMenuUtilsX::CalculateNativeInsertionPoint(this, aMenu);
183 if (MenuContainsAppMenu())
184 insertionIndex++;
185 [mNativeMenu insertItem:aMenu->NativeMenuItem() atIndex:insertionIndex];
186 }
188 return NS_OK;
190 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
191 }
193 void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex)
194 {
195 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
197 NS_ASSERTION(aIndex < mMenuArray.Length(), "Attempting submenu removal with bad index!");
199 // Our native menu and our internal menu object array might be out of sync.
200 // This happens, for example, when a submenu is hidden. Because of this we
201 // should not assume that a native submenu is hooked up.
202 NSMenuItem* nativeMenuItem = mMenuArray[aIndex]->NativeMenuItem();
203 int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
204 if (nativeMenuItemIndex != -1)
205 [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
207 mMenuArray.RemoveElementAt(aIndex);
209 NS_OBJC_END_TRY_ABORT_BLOCK;
210 }
212 void nsMenuBarX::ObserveAttributeChanged(nsIDocument* aDocument,
213 nsIContent* aContent,
214 nsIAtom* aAttribute)
215 {
216 }
218 void nsMenuBarX::ObserveContentRemoved(nsIDocument* aDocument,
219 nsIContent* aChild,
220 int32_t aIndexInContainer)
221 {
222 RemoveMenuAtIndex(aIndexInContainer);
223 }
225 void nsMenuBarX::ObserveContentInserted(nsIDocument* aDocument,
226 nsIContent* aContainer,
227 nsIContent* aChild)
228 {
229 nsMenuX* newMenu = new nsMenuX();
230 if (newMenu) {
231 nsresult rv = newMenu->Create(this, this, aChild);
232 if (NS_SUCCEEDED(rv))
233 InsertMenuAtIndex(newMenu, aContainer->IndexOf(aChild));
234 else
235 delete newMenu;
236 }
237 }
239 void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& indexString)
240 {
241 NSString* locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading())
242 length:indexString.Length()];
243 NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
244 unsigned int indexCount = [indexes count];
245 if (indexCount == 0)
246 return;
248 nsMenuX* currentMenu = NULL;
249 int targetIndex = [[indexes objectAtIndex:0] intValue];
250 int visible = 0;
251 uint32_t length = mMenuArray.Length();
252 // first find a menu in the menu bar
253 for (unsigned int i = 0; i < length; i++) {
254 nsMenuX* menu = mMenuArray[i];
255 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
256 visible++;
257 if (visible == (targetIndex + 1)) {
258 currentMenu = menu;
259 break;
260 }
261 }
262 }
264 if (!currentMenu)
265 return;
267 // fake open/close to cause lazy update to happen so submenus populate
268 currentMenu->MenuOpened();
269 currentMenu->MenuClosed();
271 // now find the correct submenu
272 for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
273 targetIndex = [[indexes objectAtIndex:i] intValue];
274 visible = 0;
275 length = currentMenu->GetItemCount();
276 for (unsigned int j = 0; j < length; j++) {
277 nsMenuObjectX* targetMenu = currentMenu->GetItemAt(j);
278 if (!targetMenu)
279 return;
280 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(targetMenu->Content())) {
281 visible++;
282 if (targetMenu->MenuObjectType() == eSubmenuObjectType && visible == (targetIndex + 1)) {
283 currentMenu = static_cast<nsMenuX*>(targetMenu);
284 // fake open/close to cause lazy update to happen
285 currentMenu->MenuOpened();
286 currentMenu->MenuClosed();
287 break;
288 }
289 }
290 }
291 }
292 }
294 // Calling this forces a full reload of the menu system, reloading all native
295 // menus and their items.
296 // Without this testing is hard because changes to the DOM affect the native
297 // menu system lazily.
298 void nsMenuBarX::ForceNativeMenuReload()
299 {
300 // tear down everything
301 while (GetMenuCount() > 0)
302 RemoveMenuAtIndex(0);
304 // construct everything
305 ConstructNativeMenus();
306 }
308 nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex)
309 {
310 if (mMenuArray.Length() <= aIndex) {
311 NS_ERROR("Requesting menu at invalid index!");
312 return NULL;
313 }
314 return mMenuArray[aIndex];
315 }
317 nsMenuX* nsMenuBarX::GetXULHelpMenu()
318 {
319 // The Help menu is usually (always?) the last one, so we start there and
320 // count back.
321 for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
322 nsMenuX* aMenu = GetMenuAt(i);
323 if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content()))
324 return aMenu;
325 }
326 return nil;
327 }
329 // On SnowLeopard and later we must tell the OS which is our Help menu.
330 // Otherwise it will only add Spotlight for Help (the Search item) to our
331 // Help menu if its label/title is "Help" -- i.e. if the menu is in English.
332 // This resolves bugs 489196 and 539317.
333 void nsMenuBarX::SetSystemHelpMenu()
334 {
335 nsMenuX* xulHelpMenu = GetXULHelpMenu();
336 if (xulHelpMenu) {
337 NSMenu* helpMenu = (NSMenu*)xulHelpMenu->NativeData();
338 if (helpMenu)
339 [NSApp setHelpMenu:helpMenu];
340 }
341 }
343 nsresult nsMenuBarX::Paint(bool aDelayed)
344 {
345 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
347 if (!aDelayed && mAwaitingDelayedPaint) {
348 return NS_OK;
349 }
350 mAwaitingDelayedPaint = false;
352 // Don't try to optimize anything in this painting by checking
353 // sLastGeckoMenuBarPainted because the menubar can be manipulated by
354 // native dialogs and sheet code and other things besides this paint method.
356 // We have to keep the same menu item for the Application menu so we keep
357 // passing it along.
358 NSMenu* outgoingMenu = [NSApp mainMenu];
359 NS_ASSERTION([outgoingMenu numberOfItems] > 0, "Main menu does not have any items, something is terribly wrong!");
361 // To work around bug 722676, we sometimes need to delay making mNativeMenu
362 // the main menu. This is an Apple bug that sometimes causes a top-level
363 // menu item to remain highlighted after pressing a Cmd+key combination that
364 // opens a new window, then closing the window. The OS temporarily
365 // highlights the appropriate top-level menu item whenever you press the
366 // Cmd+key combination for one of its submenus. (It does this by setting a
367 // "pressed" attribute on it.) The OS then uses a timer to remove this
368 // "pressed" attribute. But without our workaround we sometimes change the
369 // main menu before the timer has fired, so when it fires the menu item it
370 // was intended to unhighlight is no longer present in the main menu. This
371 // causes the item to remain semi-permanently highlighted (until you quit
372 // Firefox or navigate the main menu by hand).
373 if ((outgoingMenu != mNativeMenu) &&
374 [outgoingMenu isKindOfClass:[GeckoNSMenu class]]) {
375 if (aDelayed) {
376 [(GeckoNSMenu *)outgoingMenu setDelayResignMainMenu:false];
377 } else if ([(GeckoNSMenu *)outgoingMenu delayResignMainMenu]) {
378 PaintMenuBarAfterDelay();
379 return NS_OK;
380 }
381 }
383 if (outgoingMenu != mNativeMenu) {
384 NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
385 [outgoingMenu removeItemAtIndex:0];
386 [mNativeMenu insertItem:appMenuItem atIndex:0];
387 [appMenuItem release];
388 // Set menu bar and event target.
389 [NSApp setMainMenu:mNativeMenu];
390 }
391 SetSystemHelpMenu();
392 nsMenuBarX::sLastGeckoMenuBarPainted = this;
394 gSomeMenuBarPainted = YES;
396 return NS_OK;
398 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
399 }
401 // Used to delay a call to nsMenuBarX::Paint(). Needed to work around
402 // bug 722676.
403 void nsMenuBarX::PaintMenuBarAfterDelay()
404 {
405 mAwaitingDelayedPaint = true;
406 nsMenuBarX::sCurrentPaintDelayedMenuBar = this;
407 [mNativeMenu retain];
408 // The delay for Apple's unhighlight timer is 0.1f, so we make ours a bit longer.
409 [mNativeMenu performSelector:@selector(delayedPaintMenuBar:)
410 withObject:nil
411 afterDelay:0.15f];
412 }
414 // Returns the 'key' attribute of the 'shortcutID' object (if any) in the
415 // currently active menubar's DOM document. 'shortcutID' should be the id
416 // (i.e. the name) of a component that defines a commonly used (and
417 // localized) cmd+key shortcut, and belongs to a keyset containing similar
418 // objects. For example "key_selectAll". Returns a value that can be
419 // compared to the first character of [NSEvent charactersIgnoringModifiers]
420 // when [NSEvent modifierFlags] == NSCommandKeyMask.
421 char nsMenuBarX::GetLocalizedAccelKey(const char *shortcutID)
422 {
423 if (!sLastGeckoMenuBarPainted)
424 return 0;
426 nsCOMPtr<nsIDOMDocument> domDoc(do_QueryInterface(sLastGeckoMenuBarPainted->mDocument));
427 if (!domDoc)
428 return 0;
430 NS_ConvertASCIItoUTF16 shortcutIDStr((const char *)shortcutID);
431 nsCOMPtr<nsIDOMElement> shortcutElement;
432 domDoc->GetElementById(shortcutIDStr, getter_AddRefs(shortcutElement));
433 nsCOMPtr<nsIContent> shortcutContent = do_QueryInterface(shortcutElement);
434 if (!shortcutContent)
435 return 0;
437 nsAutoString key;
438 shortcutContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, key);
439 NS_LossyConvertUTF16toASCII keyASC(key.get());
440 const char *keyASCPtr = keyASC.get();
441 if (!keyASCPtr)
442 return 0;
443 // If keyID's 'key' attribute isn't exactly one character long, it's not
444 // what we're looking for.
445 if (strlen(keyASCPtr) != sizeof(char))
446 return 0;
447 // Make sure retval is lower case.
448 char retval = tolower(keyASCPtr[0]);
450 return retval;
451 }
453 // Hide the item in the menu by setting the 'hidden' attribute. Returns it in |outHiddenNode| so
454 // the caller can hang onto it if they so choose. It is acceptable to pass nsull
455 // for |outHiddenNode| if the caller doesn't care about the hidden node.
456 void nsMenuBarX::HideItem(nsIDOMDocument* inDoc, const nsAString & inID, nsIContent** outHiddenNode)
457 {
458 nsCOMPtr<nsIDOMElement> menuItem;
459 inDoc->GetElementById(inID, getter_AddRefs(menuItem));
460 nsCOMPtr<nsIContent> menuContent(do_QueryInterface(menuItem));
461 if (menuContent) {
462 menuContent->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, NS_LITERAL_STRING("true"), false);
463 if (outHiddenNode) {
464 *outHiddenNode = menuContent.get();
465 NS_IF_ADDREF(*outHiddenNode);
466 }
467 }
468 }
470 // Do what is necessary to conform to the Aqua guidelines for menus.
471 void nsMenuBarX::AquifyMenuBar()
472 {
473 nsCOMPtr<nsIDOMDocument> domDoc(do_QueryInterface(mContent->GetDocument()));
474 if (domDoc) {
475 // remove the "About..." item and its separator
476 HideItem(domDoc, NS_LITERAL_STRING("aboutSeparator"), nullptr);
477 HideItem(domDoc, NS_LITERAL_STRING("aboutName"), getter_AddRefs(mAboutItemContent));
478 if (!sAboutItemContent)
479 sAboutItemContent = mAboutItemContent;
481 // Hide the software update menu item, since it belongs in the application
482 // menu on Mac OS X.
483 HideItem(domDoc, NS_LITERAL_STRING("updateSeparator"), nullptr);
484 HideItem(domDoc, NS_LITERAL_STRING("checkForUpdates"), getter_AddRefs(mUpdateItemContent));
485 if (!sUpdateItemContent)
486 sUpdateItemContent = mUpdateItemContent;
488 // remove quit item and its separator
489 HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr);
490 HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent));
491 if (!sQuitItemContent)
492 sQuitItemContent = mQuitItemContent;
494 // remove prefs item and its separator, but save off the pref content node
495 // so we can invoke its command later.
496 HideItem(domDoc, NS_LITERAL_STRING("menu_PrefsSeparator"), nullptr);
497 HideItem(domDoc, NS_LITERAL_STRING("menu_preferences"), getter_AddRefs(mPrefItemContent));
498 if (!sPrefItemContent)
499 sPrefItemContent = mPrefItemContent;
501 // hide items that we use for the Application menu
502 HideItem(domDoc, NS_LITERAL_STRING("menu_mac_services"), nullptr);
503 HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_app"), nullptr);
504 HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_others"), nullptr);
505 HideItem(domDoc, NS_LITERAL_STRING("menu_mac_show_all"), nullptr);
506 }
507 }
509 // for creating menu items destined for the Application menu
510 NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* inMenu, const nsAString& nodeID, SEL action,
511 int tag, NativeMenuItemTarget* target)
512 {
513 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
515 nsCOMPtr<nsIDocument> doc = inMenu->Content()->GetDocument();
516 if (!doc)
517 return nil;
519 nsCOMPtr<nsIDOMDocument> domdoc(do_QueryInterface(doc));
520 if (!domdoc)
521 return nil;
523 // Get information from the gecko menu item
524 nsAutoString label;
525 nsAutoString modifiers;
526 nsAutoString key;
527 nsCOMPtr<nsIDOMElement> menuItem;
528 domdoc->GetElementById(nodeID, getter_AddRefs(menuItem));
529 if (menuItem) {
530 menuItem->GetAttribute(NS_LITERAL_STRING("label"), label);
531 menuItem->GetAttribute(NS_LITERAL_STRING("modifiers"), modifiers);
532 menuItem->GetAttribute(NS_LITERAL_STRING("key"), key);
533 }
534 else {
535 return nil;
536 }
538 // Get more information about the key equivalent. Start by
539 // finding the key node we need.
540 NSString* keyEquiv = nil;
541 unsigned int macKeyModifiers = 0;
542 if (!key.IsEmpty()) {
543 nsCOMPtr<nsIDOMElement> keyElement;
544 domdoc->GetElementById(key, getter_AddRefs(keyElement));
545 if (keyElement) {
546 nsCOMPtr<nsIContent> keyContent (do_QueryInterface(keyElement));
547 // first grab the key equivalent character
548 nsAutoString keyChar(NS_LITERAL_STRING(" "));
549 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
550 if (!keyChar.EqualsLiteral(" ")) {
551 keyEquiv = [[NSString stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
552 length:keyChar.Length()] lowercaseString];
553 }
554 // now grab the key equivalent modifiers
555 nsAutoString modifiersStr;
556 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
557 uint8_t geckoModifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
558 macKeyModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
559 }
560 }
561 // get the label into NSString-form
562 NSString* labelString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
563 length:label.Length()];
565 if (!labelString)
566 labelString = @"";
567 if (!keyEquiv)
568 keyEquiv = @"";
570 // put together the actual NSMenuItem
571 NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString action:action keyEquivalent:keyEquiv];
573 [newMenuItem setTag:tag];
574 [newMenuItem setTarget:target];
575 [newMenuItem setKeyEquivalentModifierMask:macKeyModifiers];
577 MenuItemInfo * info = [[MenuItemInfo alloc] initWithMenuGroupOwner:this];
578 [newMenuItem setRepresentedObject:info];
579 [info release];
581 return newMenuItem;
583 NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
584 }
586 // build the Application menu shared by all menu bars
587 nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu)
588 {
589 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
591 // At this point, the application menu is the application menu from
592 // the nib in cocoa widgets. We do not have a way to create an application
593 // menu manually, so we grab the one from the nib and use that.
594 sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
596 /*
597 We support the following menu items here:
599 Menu Item DOM Node ID Notes
601 ========================
602 = About This App = <- aboutName
603 = Check for Updates... = <- checkForUpdates
604 ========================
605 = Preferences... = <- menu_preferences
606 ========================
607 = Services > = <- menu_mac_services <- (do not define key equivalent)
608 ========================
609 = Hide App = <- menu_mac_hide_app
610 = Hide Others = <- menu_mac_hide_others
611 = Show All = <- menu_mac_show_all
612 ========================
613 = Quit = <- menu_FileQuitItem
614 ========================
616 If any of them are ommitted from the application's DOM, we just don't add
617 them. We always add a "Quit" item, but if an app developer does not provide a
618 DOM node with the right ID for the Quit item, we add it in English. App
619 developers need only add each node with a label and a key equivalent (if they
620 want one). Other attributes are optional. Like so:
622 <menuitem id="menu_preferences"
623 label="&preferencesCmdMac.label;"
624 key="open_prefs_key"/>
626 We need to use this system for localization purposes, until we have a better way
627 to define the Application menu to be used on Mac OS X.
628 */
630 if (sApplicationMenu) {
631 // This code reads attributes we are going to care about from the DOM elements
633 NSMenuItem *itemBeingAdded = nil;
634 BOOL addAboutSeparator = FALSE;
636 // Add the About menu item
637 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("aboutName"), @selector(menuItemHit:),
638 eCommand_ID_About, nsMenuBarX::sNativeEventTarget);
639 if (itemBeingAdded) {
640 [sApplicationMenu addItem:itemBeingAdded];
641 [itemBeingAdded release];
642 itemBeingAdded = nil;
644 addAboutSeparator = TRUE;
645 }
647 // Add the software update menu item
648 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("checkForUpdates"), @selector(menuItemHit:),
649 eCommand_ID_Update, nsMenuBarX::sNativeEventTarget);
650 if (itemBeingAdded) {
651 [sApplicationMenu addItem:itemBeingAdded];
652 [itemBeingAdded release];
653 itemBeingAdded = nil;
655 addAboutSeparator = TRUE;
656 }
658 // Add separator if either the About item or software update item exists
659 if (addAboutSeparator)
660 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
662 // Add the Preferences menu item
663 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_preferences"), @selector(menuItemHit:),
664 eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
665 if (itemBeingAdded) {
666 [sApplicationMenu addItem:itemBeingAdded];
667 [itemBeingAdded release];
668 itemBeingAdded = nil;
670 // Add separator after Preferences menu
671 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
672 }
674 // Add Services menu item
675 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_services"), nil,
676 0, nil);
677 if (itemBeingAdded) {
678 [sApplicationMenu addItem:itemBeingAdded];
680 // set this menu item up as the Mac OS X Services menu
681 NSMenu* servicesMenu = [[GeckoServicesNSMenu alloc] initWithTitle:@""];
682 [itemBeingAdded setSubmenu:servicesMenu];
683 [NSApp setServicesMenu:servicesMenu];
685 [itemBeingAdded release];
686 itemBeingAdded = nil;
688 // Add separator after Services menu
689 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
690 }
692 BOOL addHideShowSeparator = FALSE;
694 // Add menu item to hide this application
695 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_hide_app"), @selector(menuItemHit:),
696 eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
697 if (itemBeingAdded) {
698 [sApplicationMenu addItem:itemBeingAdded];
699 [itemBeingAdded release];
700 itemBeingAdded = nil;
702 addHideShowSeparator = TRUE;
703 }
705 // Add menu item to hide other applications
706 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_hide_others"), @selector(menuItemHit:),
707 eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
708 if (itemBeingAdded) {
709 [sApplicationMenu addItem:itemBeingAdded];
710 [itemBeingAdded release];
711 itemBeingAdded = nil;
713 addHideShowSeparator = TRUE;
714 }
716 // Add menu item to show all applications
717 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_show_all"), @selector(menuItemHit:),
718 eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
719 if (itemBeingAdded) {
720 [sApplicationMenu addItem:itemBeingAdded];
721 [itemBeingAdded release];
722 itemBeingAdded = nil;
724 addHideShowSeparator = TRUE;
725 }
727 // Add a separator after the hide/show menus if at least one exists
728 if (addHideShowSeparator)
729 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
731 // Add quit menu item
732 itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_FileQuitItem"), @selector(menuItemHit:),
733 eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
734 if (itemBeingAdded) {
735 [sApplicationMenu addItem:itemBeingAdded];
736 [itemBeingAdded release];
737 itemBeingAdded = nil;
738 }
739 else {
740 // the current application does not have a DOM node for "Quit". Add one
741 // anyway, in English.
742 NSMenuItem* defaultQuitItem = [[[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(menuItemHit:)
743 keyEquivalent:@"q"] autorelease];
744 [defaultQuitItem setTarget:nsMenuBarX::sNativeEventTarget];
745 [defaultQuitItem setTag:eCommand_ID_Quit];
746 [sApplicationMenu addItem:defaultQuitItem];
747 }
748 }
750 return (sApplicationMenu) ? NS_OK : NS_ERROR_FAILURE;
752 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
753 }
755 void nsMenuBarX::SetParent(nsIWidget* aParent)
756 {
757 mParentWindow = aParent;
758 }
761 //
762 // Objective-C class used to allow us to have keyboard commands
763 // look like they are doing something but actually do nothing.
764 // We allow mouse actions to work normally.
765 //
767 // Controls whether or not native menu items should invoke their commands.
768 static BOOL gMenuItemsExecuteCommands = YES;
770 @implementation GeckoNSMenu
772 - (id)initWithTitle:(NSString *)aTitle
773 {
774 if (self = [super initWithTitle:aTitle]) {
775 mMenuBarOwner = nullptr;
776 mDelayResignMainMenu = false;
777 }
778 return self;
779 }
781 - (id)initWithTitle:(NSString *)aTitle andMenuBarOwner:(nsMenuBarX *)aMenuBarOwner
782 {
783 if (self = [super initWithTitle:aTitle]) {
784 mMenuBarOwner = aMenuBarOwner;
785 mDelayResignMainMenu = false;
786 }
787 return self;
788 }
790 - (void)resetMenuBarOwner
791 {
792 mMenuBarOwner = nil;
793 }
795 - (bool)delayResignMainMenu
796 {
797 return mDelayResignMainMenu;
798 }
800 - (void)setDelayResignMainMenu:(bool)aShouldDelay
801 {
802 mDelayResignMainMenu = aShouldDelay;
803 }
805 // Used to delay a call to nsMenuBarX::Paint(). Needed to work around
806 // bug 722676.
807 - (void)delayedPaintMenuBar:(id)unused
808 {
809 if (mMenuBarOwner) {
810 if (mMenuBarOwner == nsMenuBarX::sCurrentPaintDelayedMenuBar) {
811 mMenuBarOwner->Paint(true);
812 nsMenuBarX::sCurrentPaintDelayedMenuBar = nullptr;
813 } else {
814 mMenuBarOwner->ResetAwaitingDelayedPaint();
815 }
816 }
817 [self release];
818 }
820 // Undocumented method, present unchanged since OS X 10.6, used to temporarily
821 // highlight a top-level menu item when an appropriate Cmd+key combination is
822 // pressed.
823 - (void)_performActionWithHighlightingForItemAtIndex:(NSInteger)index;
824 {
825 NSMenu *mainMenu = [NSApp mainMenu];
826 if ([mainMenu isKindOfClass:[GeckoNSMenu class]]) {
827 [(GeckoNSMenu *)mainMenu setDelayResignMainMenu:true];
828 }
829 [super _performActionWithHighlightingForItemAtIndex:index];
830 }
832 // Keyboard commands should not cause menu items to invoke their
833 // commands when there is a key window because we'd rather send
834 // the keyboard command to the window. We still have the menus
835 // go through the mechanics so they'll give the proper visual
836 // feedback.
837 - (BOOL)performKeyEquivalent:(NSEvent *)theEvent
838 {
839 // We've noticed that Mac OS X expects this check in subclasses before
840 // calling NSMenu's "performKeyEquivalent:".
841 //
842 // There is no case in which we'd need to do anything or return YES
843 // when we have no items so we can just do this check first.
844 if ([self numberOfItems] <= 0) {
845 return NO;
846 }
848 NSWindow *keyWindow = [NSApp keyWindow];
850 // If there is no key window then just behave normally. This
851 // probably means that this menu is associated with Gecko's
852 // hidden window.
853 if (!keyWindow) {
854 return [super performKeyEquivalent:theEvent];
855 }
857 // Plugins normally eat all keyboard commands, this hack mitigates
858 // the problem.
859 BOOL handleForPluginHack = NO;
860 NSResponder *firstResponder = [keyWindow firstResponder];
861 if (firstResponder &&
862 [firstResponder isKindOfClass:[ChildView class]] &&
863 [(ChildView*)firstResponder isPluginView]) {
864 handleForPluginHack = YES;
865 // Maintain a list of cmd+key combinations that we never act on (in the
866 // browser) when the keyboard focus is in a plugin. What a particular
867 // cmd+key combo means here (to the browser) is governed by browser.dtd,
868 // which "contains the browser main menu items".
869 UInt32 modifierFlags = [theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask;
870 if (modifierFlags == NSCommandKeyMask) {
871 NSString *unmodchars = [theEvent charactersIgnoringModifiers];
872 if ([unmodchars length] == 1) {
873 if ([unmodchars characterAtIndex:0] == nsMenuBarX::GetLocalizedAccelKey("key_selectAll")) {
874 handleForPluginHack = NO;
875 }
876 }
877 }
878 }
880 gMenuItemsExecuteCommands = handleForPluginHack;
881 [super performKeyEquivalent:theEvent];
882 gMenuItemsExecuteCommands = YES; // return to default
884 // Return YES if we invoked a command and there is now no key window or we changed
885 // the first responder. In this case we do not want to propagate the event because
886 // we don't want it handled again.
887 if (handleForPluginHack) {
888 if (![NSApp keyWindow] || [[NSApp keyWindow] firstResponder] != firstResponder) {
889 return YES;
890 }
891 }
893 // Return NO so that we can handle the event via NSView's "keyDown:".
894 return NO;
895 }
897 @end
899 //
900 // Objective-C class used as action target for menu items
901 //
903 @implementation NativeMenuItemTarget
905 // called when some menu item in this menu gets hit
906 -(IBAction)menuItemHit:(id)sender
907 {
908 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
910 // menuGroupOwner below is an nsMenuBarX object, which we sometimes access
911 // after it's been deleted, causing crashes (see bug 704866 and bug 670914).
912 // To fix this "correctly", in nsMenuBarX::~nsMenuBarX() we'd need to
913 // iterate through every NSMenuItem in nsMenuBarX::mNativeMenu and its
914 // submenus, which might be quite time consuming. (For every NSMenuItem
915 // that has a "representedObject" that's a MenuItemInfo object, we'd need
916 // need to null out its "menuGroupOwner" if it's the same as the nsMenuBarX
917 // object being destroyed.) But if the nsMenuBarX object being destroyed
918 // corresponds to the currently focused window, it's likely that the
919 // nsMenuBarX destructor will null out sLastGeckoMenuBarPainted. So we can
920 // probably eliminate most of these crashes if we use this variable being
921 // null as an indicator that we're likely to crash below when we dereference
922 // menuGroupOwner.
923 if (!nsMenuBarX::sLastGeckoMenuBarPainted) {
924 return;
925 }
927 if (!gMenuItemsExecuteCommands) {
928 return;
929 }
931 int tag = [sender tag];
933 MenuItemInfo* info = [sender representedObject];
934 if (!info)
935 return;
937 nsMenuGroupOwnerX* menuGroupOwner = [info menuGroupOwner];
938 if (!menuGroupOwner)
939 return;
941 nsMenuBarX* menuBar = nullptr;
942 if (menuGroupOwner->MenuObjectType() == eMenuBarObjectType)
943 menuBar = static_cast<nsMenuBarX*>(menuGroupOwner);
945 // Do special processing if this is for an app-global command.
946 if (tag == eCommand_ID_About) {
947 nsIContent* mostSpecificContent = sAboutItemContent;
948 if (menuBar && menuBar->mAboutItemContent)
949 mostSpecificContent = menuBar->mAboutItemContent;
950 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
951 return;
952 }
953 else if (tag == eCommand_ID_Update) {
954 nsIContent* mostSpecificContent = sUpdateItemContent;
955 if (menuBar && menuBar->mUpdateItemContent)
956 mostSpecificContent = menuBar->mUpdateItemContent;
957 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
958 }
959 else if (tag == eCommand_ID_Prefs) {
960 nsIContent* mostSpecificContent = sPrefItemContent;
961 if (menuBar && menuBar->mPrefItemContent)
962 mostSpecificContent = menuBar->mPrefItemContent;
963 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
964 return;
965 }
966 else if (tag == eCommand_ID_HideApp) {
967 [NSApp hide:sender];
968 return;
969 }
970 else if (tag == eCommand_ID_HideOthers) {
971 [NSApp hideOtherApplications:sender];
972 return;
973 }
974 else if (tag == eCommand_ID_ShowAll) {
975 [NSApp unhideAllApplications:sender];
976 return;
977 }
978 else if (tag == eCommand_ID_Quit) {
979 nsIContent* mostSpecificContent = sQuitItemContent;
980 if (menuBar && menuBar->mQuitItemContent)
981 mostSpecificContent = menuBar->mQuitItemContent;
982 // If we have some content for quit we execute it. Otherwise we send a native app terminate
983 // message. If you want to stop a quit from happening, provide quit content and return
984 // the event as unhandled.
985 if (mostSpecificContent) {
986 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
987 }
988 else {
989 [NSApp terminate:nil];
990 }
991 return;
992 }
994 // given the commandID, look it up in our hashtable and dispatch to
995 // that menu item.
996 if (menuGroupOwner) {
997 nsMenuItemX* menuItem = menuGroupOwner->GetMenuItemForCommandID(static_cast<uint32_t>(tag));
998 if (menuItem)
999 menuItem->DoCommand();
1000 }
1002 NS_OBJC_END_TRY_ABORT_BLOCK;
1003 }
1005 @end
1007 // Objective-C class used for menu items on the Services menu to allow Gecko
1008 // to override their standard behavior in order to stop key equivalents from
1009 // firing in certain instances. When gMenuItemsExecuteCommands is NO, we return
1010 // a dummy target and action instead of the actual target and action.
1012 @implementation GeckoServicesNSMenuItem
1014 - (id) target
1015 {
1016 id realTarget = [super target];
1017 if (gMenuItemsExecuteCommands)
1018 return realTarget;
1019 else
1020 return realTarget ? self : nil;
1021 }
1023 - (SEL) action
1024 {
1025 SEL realAction = [super action];
1026 if (gMenuItemsExecuteCommands)
1027 return realAction;
1028 else
1029 return realAction ? @selector(_doNothing:) : NULL;
1030 }
1032 - (void) _doNothing:(id)sender
1033 {
1034 }
1036 @end
1038 // Objective-C class used as the Services menu so that Gecko can override the
1039 // standard behavior of the Services menu in order to stop key equivalents
1040 // from firing in certain instances.
1042 @implementation GeckoServicesNSMenu
1044 - (void)addItem:(NSMenuItem *)newItem
1045 {
1046 [self _overrideClassOfMenuItem:newItem];
1047 [super addItem:newItem];
1048 }
1050 - (NSMenuItem *)addItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv
1051 {
1052 NSMenuItem * newItem = [super addItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv];
1053 [self _overrideClassOfMenuItem:newItem];
1054 return newItem;
1055 }
1057 - (void)insertItem:(NSMenuItem *)newItem atIndex:(NSInteger)index
1058 {
1059 [self _overrideClassOfMenuItem:newItem];
1060 [super insertItem:newItem atIndex:index];
1061 }
1063 - (NSMenuItem *)insertItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv atIndex:(NSInteger)index
1064 {
1065 NSMenuItem * newItem = [super insertItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv atIndex:index];
1066 [self _overrideClassOfMenuItem:newItem];
1067 return newItem;
1068 }
1070 - (void) _overrideClassOfMenuItem:(NSMenuItem *)menuItem
1071 {
1072 if ([menuItem class] == [NSMenuItem class])
1073 object_setClass(menuItem, [GeckoServicesNSMenuItem class]);
1074 }
1076 @end