widget/cocoa/nsStandaloneNativeMenu.mm
author Ryan VanderMeulen <ryanvm@gmail.com>
Thu, 14 Aug 2014 17:23:10 -0400
changeset 222544 1d41a9159e5809e937c8abd7490dd845832652c4
parent 216892 a0e306c8d5be9f67b2320f55bbfbc0d4e27bf212
child 261891 133ec7304f2208e89f02ed943480a1b1f02188c9
permissions -rw-r--r--
Backed out changesets 5b1a3161f614, 17a9673ed782, and a9b8c346d295 (bug 1052052) for B2G non-unified bustage.

/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#import <Cocoa/Cocoa.h>

#include "nsStandaloneNativeMenu.h"
#include "nsMenuUtilsX.h"
#include "nsIDOMElement.h"
#include "nsIMutationObserver.h"
#include "nsGkAtoms.h"
#include "nsObjCExceptions.h"


NS_IMPL_ISUPPORTS_INHERITED(nsStandaloneNativeMenu, nsMenuGroupOwnerX,
                            nsIMutationObserver, nsIStandaloneNativeMenu)

nsStandaloneNativeMenu::nsStandaloneNativeMenu()
: mMenu(nullptr)
, mContainerStatusBarItem(nil)
{
}

nsStandaloneNativeMenu::~nsStandaloneNativeMenu()
{
  if (mMenu) delete mMenu;
}

NS_IMETHODIMP
nsStandaloneNativeMenu::Init(nsIDOMElement * aDOMElement)
{
  NS_ASSERTION(mMenu == nullptr, "nsNativeMenu::Init - mMenu not null!");

  nsresult rv;

  nsCOMPtr<nsIContent> content = do_QueryInterface(aDOMElement, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsIAtom * tag = content->Tag();
  if (!content->IsXUL() ||
      (tag != nsGkAtoms::menu && tag != nsGkAtoms::menupopup))
    return NS_ERROR_FAILURE;

  rv = nsMenuGroupOwnerX::Create(content);
  if (NS_FAILED(rv))
    return rv;

  mMenu = new nsMenuX();
  rv = mMenu->Create(this, this, content);
  if (NS_FAILED(rv)) {
    delete mMenu;
    mMenu = nullptr;
    return rv;
  }

  mMenu->SetupIcon();

  return NS_OK;
}

static void
UpdateMenu(nsMenuX * aMenu)
{
  aMenu->MenuOpened();
  aMenu->MenuClosed();

  uint32_t itemCount = aMenu->GetItemCount();
  for (uint32_t i = 0; i < itemCount; i++) {
    nsMenuObjectX * menuObject = aMenu->GetItemAt(i);
    if (menuObject->MenuObjectType() == eSubmenuObjectType) {
      UpdateMenu(static_cast<nsMenuX*>(menuObject));
    }
  }
}

NS_IMETHODIMP
nsStandaloneNativeMenu::MenuWillOpen(bool * aResult)
{
  NS_ASSERTION(mMenu != nullptr, "nsStandaloneNativeMenu::OnOpen - mMenu is null!");

  // Force an update on the mMenu by faking an open/close on all of
  // its submenus.
  UpdateMenu(mMenu);

  *aResult = true;
  return NS_OK;
}

NS_IMETHODIMP
nsStandaloneNativeMenu::GetNativeMenu(void ** aVoidPointer)
{
  if (mMenu) {
    *aVoidPointer = mMenu->NativeData();
    [[(NSObject *)(*aVoidPointer) retain] autorelease];
    return NS_OK;
  }  else {
    *aVoidPointer = nullptr;
    return NS_ERROR_NOT_INITIALIZED;
  }
}

static NSMenuItem *
NativeMenuItemWithLocation(NSMenu * currentSubmenu, NSString * locationString)
{
  NSArray * indexes = [locationString componentsSeparatedByString:@"|"];
  NSUInteger indexCount = [indexes count];
  if (indexCount == 0)
    return nil;

  for (NSUInteger i = 0; i < indexCount; i++) {
    NSInteger targetIndex = [[indexes objectAtIndex:i] integerValue];
    NSInteger itemCount = [currentSubmenu numberOfItems];
    if (targetIndex < itemCount) {
      NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex];

      // If this is the last index, just return the menu item.
      if (i == (indexCount - 1))
        return menuItem;

      // If this is not the last index, find the submenu and keep going.
      if ([menuItem hasSubmenu])
        currentSubmenu = [menuItem submenu];
      else
        return nil;
    }
  }

  return nil;
}

NS_IMETHODIMP
nsStandaloneNativeMenu::ActivateNativeMenuItemAt(const nsAString& indexString)
{
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
  
  if (!mMenu)
    return NS_ERROR_NOT_INITIALIZED;

  NSString * locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading())
                                                      length:indexString.Length()];
  NSMenu * menu = static_cast<NSMenu *> (mMenu->NativeData());
  NSMenuItem * item = NativeMenuItemWithLocation(menu, locationString);

  // We can't perform an action on an item with a submenu, that will raise
  // an obj-c exception.
  if (item && ![item hasSubmenu]) {
    NSMenu * parent = [item menu];
    if (parent) {
      // NSLog(@"Performing action for native menu item titled: %@\n",
      //       [[currentSubmenu itemAtIndex:targetIndex] title]);
      [parent performActionForItemAtIndex:[parent indexOfItem:item]];
      return NS_OK;
    }
  }

  return NS_ERROR_FAILURE;
  
  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}

NS_IMETHODIMP
nsStandaloneNativeMenu::ForceUpdateNativeMenuAt(const nsAString& indexString)
{
  if (!mMenu)
    return NS_ERROR_NOT_INITIALIZED;

  NSString* locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading())
                                                     length:indexString.Length()];
  NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
  unsigned int indexCount = [indexes count];
  if (indexCount == 0)
    return NS_OK;

  nsMenuX* currentMenu = mMenu;

  // now find the correct submenu
  for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
    int targetIndex = [[indexes objectAtIndex:i] intValue];
    int visible = 0;
    uint32_t length = currentMenu->GetItemCount();
    for (unsigned int j = 0; j < length; j++) {
      nsMenuObjectX* targetMenu = currentMenu->GetItemAt(j);
      if (!targetMenu)
        return NS_OK;
      if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(targetMenu->Content())) {
        visible++;
        if (targetMenu->MenuObjectType() == eSubmenuObjectType && visible == (targetIndex + 1)) {
          currentMenu = static_cast<nsMenuX*>(targetMenu);
          // fake open/close to cause lazy update to happen
          currentMenu->MenuOpened();
          currentMenu->MenuClosed();
          break;
        }
      }
    }
  }

  return NS_OK;
}

void
nsStandaloneNativeMenu::IconUpdated()
{
  if (mContainerStatusBarItem) {
    [mContainerStatusBarItem setImage:[mMenu->NativeMenuItem() image]];
  }
}

void
nsStandaloneNativeMenu::SetContainerStatusBarItem(NSStatusItem* aItem)
{
  mContainerStatusBarItem = aItem;
  IconUpdated();
}