summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsMenuItemIconX.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsMenuItemIconX.mm')
-rw-r--r--widget/cocoa/nsMenuItemIconX.mm466
1 files changed, 466 insertions, 0 deletions
diff --git a/widget/cocoa/nsMenuItemIconX.mm b/widget/cocoa/nsMenuItemIconX.mm
new file mode 100644
index 000000000..7589c279e
--- /dev/null
+++ b/widget/cocoa/nsMenuItemIconX.mm
@@ -0,0 +1,466 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * Retrieves and displays icons in native menu items on Mac OS X.
+ */
+
+/* exception_defines.h defines 'try' to 'if (true)' which breaks objective-c
+ exceptions and produces errors like: error: unexpected '@' in program'.
+ If we define __EXCEPTIONS exception_defines.h will avoid doing this.
+
+ See bug 666609 for more information.
+
+ We use <limits> to get the libstdc++ version. */
+#include <limits>
+#if __GLIBCXX__ <= 20070719
+#ifndef __EXCEPTIONS
+#define __EXCEPTIONS
+#endif
+#endif
+
+#include "nsMenuItemIconX.h"
+#include "nsObjCExceptions.h"
+#include "nsIContent.h"
+#include "nsIDocument.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMElement.h"
+#include "nsICSSDeclaration.h"
+#include "nsIDOMCSSValue.h"
+#include "nsIDOMCSSPrimitiveValue.h"
+#include "nsIDOMRect.h"
+#include "nsThreadUtils.h"
+#include "nsToolkit.h"
+#include "nsNetUtil.h"
+#include "imgLoader.h"
+#include "imgRequestProxy.h"
+#include "nsMenuItemX.h"
+#include "gfxPlatform.h"
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+#include "nsContentUtils.h"
+#include "nsIContentPolicy.h"
+
+using mozilla::dom::Element;
+using mozilla::gfx::SourceSurface;
+
+static const uint32_t kIconWidth = 16;
+static const uint32_t kIconHeight = 16;
+
+typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect,
+ GetBottom, (nsIDOMCSSPrimitiveValue**));
+
+NS_IMPL_ISUPPORTS(nsMenuItemIconX, imgINotificationObserver)
+
+nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem,
+ nsIContent* aContent,
+ NSMenuItem* aNativeMenuItem)
+: mContent(aContent)
+, mMenuObject(aMenuItem)
+, mLoadedIcon(false)
+, mSetIcon(false)
+, mNativeMenuItem(aNativeMenuItem)
+{
+ // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem);
+}
+
+nsMenuItemIconX::~nsMenuItemIconX()
+{
+ if (mIconRequest)
+ mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
+}
+
+// Called from mMenuObjectX's destructor, to prevent us from outliving it
+// (as might otherwise happen if calls to our imgINotificationObserver methods
+// are still outstanding). mMenuObjectX owns our nNativeMenuItem.
+void nsMenuItemIconX::Destroy()
+{
+ if (mIconRequest) {
+ mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ mIconRequest = nullptr;
+ }
+ mMenuObject = nullptr;
+ mNativeMenuItem = nil;
+}
+
+nsresult
+nsMenuItemIconX::SetupIcon()
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ // Still don't have one, then something is wrong, get out of here.
+ if (!mNativeMenuItem) {
+ NS_ERROR("No native menu item");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIURI> iconURI;
+ nsresult rv = GetIconURI(getter_AddRefs(iconURI));
+ if (NS_FAILED(rv)) {
+ // There is no icon for this menu item. An icon might have been set
+ // earlier. Clear it.
+ [mNativeMenuItem setImage:nil];
+
+ return NS_OK;
+ }
+
+ rv = LoadIcon(iconURI);
+ if (NS_FAILED(rv)) {
+ // There is no icon for this menu item, as an error occurred while loading it.
+ // An icon might have been set earlier or the place holder icon may have
+ // been set. Clear it.
+ [mNativeMenuItem setImage:nil];
+ }
+ return rv;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+static int32_t
+GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
+{
+ nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
+ (aRect->*aMethod)(getter_AddRefs(dimensionValue));
+ if (!dimensionValue)
+ return -1;
+
+ uint16_t primitiveType;
+ nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
+ if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
+ return -1;
+
+ float dimension = 0;
+ rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
+ &dimension);
+ if (NS_FAILED(rv))
+ return -1;
+
+ return NSToIntRound(dimension);
+}
+
+nsresult
+nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
+{
+ if (!mMenuObject)
+ return NS_ERROR_FAILURE;
+
+ // Mac native menu items support having both a checkmark and an icon
+ // simultaneously, but this is unheard of in the cross-platform toolkit,
+ // seemingly because the win32 theme is unable to cope with both at once.
+ // The downside is that it's possible to get a menu item marked with a
+ // native checkmark and a checkmark for an icon. Head off that possibility
+ // by pretending that no icon exists if this is a checkable menu item.
+ if (mMenuObject->MenuObjectType() == eMenuItemObjectType) {
+ nsMenuItemX* menuItem = static_cast<nsMenuItemX*>(mMenuObject);
+ if (menuItem->GetMenuItemType() != eRegularMenuItemType)
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mContent)
+ return NS_ERROR_FAILURE;
+
+ // First, look at the content node's "image" attribute.
+ nsAutoString imageURIString;
+ bool hasImageAttr = mContent->GetAttr(kNameSpaceID_None,
+ nsGkAtoms::image,
+ imageURIString);
+
+ nsresult rv;
+ nsCOMPtr<nsIDOMCSSValue> cssValue;
+ nsCOMPtr<nsICSSDeclaration> cssStyleDecl;
+ nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
+ uint16_t primitiveType;
+ if (!hasImageAttr) {
+ // If the content node has no "image" attribute, get the
+ // "list-style-image" property from CSS.
+ nsCOMPtr<nsIDocument> document = mContent->GetComposedDoc();
+ if (!document)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsPIDOMWindowInner> window = document->GetInnerWindow();
+ if (!window)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<Element> domElement = do_QueryInterface(mContent);
+ if (!domElement)
+ return NS_ERROR_FAILURE;
+
+ ErrorResult dummy;
+ cssStyleDecl = window->GetComputedStyle(*domElement, EmptyString(), dummy);
+ dummy.SuppressException();
+ if (!cssStyleDecl)
+ return NS_ERROR_FAILURE;
+
+ NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image");
+ rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage,
+ getter_AddRefs(cssValue));
+ if (NS_FAILED(rv)) return rv;
+
+ primitiveValue = do_QueryInterface(cssValue);
+ if (!primitiveValue) return NS_ERROR_FAILURE;
+
+ rv = primitiveValue->GetPrimitiveType(&primitiveType);
+ if (NS_FAILED(rv)) return rv;
+ if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI)
+ return NS_ERROR_FAILURE;
+
+ rv = primitiveValue->GetStringValue(imageURIString);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Empty the mImageRegionRect initially as the image region CSS could
+ // have been changed and now have an error or have been removed since the
+ // last GetIconURI call.
+ mImageRegionRect.SetEmpty();
+
+ // If this menu item shouldn't have an icon, the string will be empty,
+ // and NS_NewURI will fail.
+ nsCOMPtr<nsIURI> iconURI;
+ rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
+ if (NS_FAILED(rv)) return rv;
+
+ *aIconURI = iconURI;
+ NS_ADDREF(*aIconURI);
+
+ if (!hasImageAttr) {
+ // Check if the icon has a specified image region so that it can be
+ // cropped appropriately before being displayed.
+ NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region");
+ rv = cssStyleDecl->GetPropertyCSSValue(imageRegion,
+ getter_AddRefs(cssValue));
+ // Just return NS_OK if there if there is a failure due to no
+ // moz-image region specified so the whole icon will be drawn anyway.
+ if (NS_FAILED(rv)) return NS_OK;
+
+ primitiveValue = do_QueryInterface(cssValue);
+ if (!primitiveValue) return NS_OK;
+
+ rv = primitiveValue->GetPrimitiveType(&primitiveType);
+ if (NS_FAILED(rv)) return NS_OK;
+ if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT)
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMRect> imageRegionRect;
+ rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect));
+ if (NS_FAILED(rv)) return NS_OK;
+
+ if (imageRegionRect) {
+ // Return NS_ERROR_FAILURE if the image region is invalid so the image
+ // is not drawn, and behavior is similar to XUL menus.
+ int32_t bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom);
+ int32_t right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight);
+ int32_t top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop);
+ int32_t left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft);
+
+ if (top < 0 || left < 0 || bottom <= top || right <= left)
+ return NS_ERROR_FAILURE;
+
+ mImageRegionRect.SetRect(left, top, right - left, bottom - top);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ if (mIconRequest) {
+ // Another icon request is already in flight. Kill it.
+ mIconRequest->Cancel(NS_BINDING_ABORTED);
+ mIconRequest = nullptr;
+ }
+
+ mLoadedIcon = false;
+
+ if (!mContent) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIDocument> document = mContent->OwnerDoc();
+
+ nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup();
+ if (!loadGroup) return NS_ERROR_FAILURE;
+
+ RefPtr<imgLoader> loader = nsContentUtils::GetImgLoaderForDocument(document);
+ if (!loader) return NS_ERROR_FAILURE;
+
+ if (!mSetIcon) {
+ // Set a completely transparent 16x16 image as the icon on this menu item
+ // as a placeholder. This keeps the menu item text displayed in the same
+ // position that it will be displayed when the real icon is loaded, and
+ // prevents it from jumping around or looking misaligned.
+
+ static bool sInitializedPlaceholder;
+ static NSImage* sPlaceholderIconImage;
+ if (!sInitializedPlaceholder) {
+ sInitializedPlaceholder = true;
+
+ // Note that we only create the one and reuse it forever, so this is not a leak.
+ sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)];
+ }
+
+ if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
+
+ if (mNativeMenuItem)
+ [mNativeMenuItem setImage:sPlaceholderIconImage];
+ }
+
+ nsresult rv = loader->LoadImage(aIconURI, nullptr, nullptr,
+ mozilla::net::RP_Default,
+ nullptr, loadGroup, this,
+ nullptr, nullptr, nsIRequest::LOAD_NORMAL, nullptr,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE, EmptyString(),
+ getter_AddRefs(mIconRequest));
+ if (NS_FAILED(rv)) return rv;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+//
+// imgINotificationObserver
+//
+
+NS_IMETHODIMP
+nsMenuItemIconX::Notify(imgIRequest* aRequest,
+ int32_t aType,
+ const nsIntRect* aData)
+{
+ if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+ // Make sure the image loaded successfully.
+ uint32_t status = imgIRequest::STATUS_ERROR;
+ if (NS_FAILED(aRequest->GetImageStatus(&status)) ||
+ (status & imgIRequest::STATUS_ERROR)) {
+ mIconRequest->Cancel(NS_BINDING_ABORTED);
+ mIconRequest = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ MOZ_ASSERT(image);
+
+ // Ask the image to decode at its intrinsic size.
+ int32_t width = 0, height = 0;
+ image->GetWidth(&width);
+ image->GetHeight(&height);
+ image->RequestDecodeForSize(nsIntSize(width, height), imgIContainer::FLAG_NONE);
+ }
+
+ if (aType == imgINotificationObserver::FRAME_COMPLETE) {
+ return OnFrameComplete(aRequest);
+ }
+
+ if (aType == imgINotificationObserver::DECODE_COMPLETE) {
+ if (mIconRequest && mIconRequest == aRequest) {
+ mIconRequest->Cancel(NS_BINDING_ABORTED);
+ mIconRequest = nullptr;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsMenuItemIconX::OnFrameComplete(imgIRequest* aRequest)
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ if (aRequest != mIconRequest)
+ return NS_ERROR_FAILURE;
+
+ // Only support one frame.
+ if (mLoadedIcon)
+ return NS_OK;
+
+ if (!mNativeMenuItem)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<imgIContainer> imageContainer;
+ aRequest->GetImage(getter_AddRefs(imageContainer));
+ if (!imageContainer) {
+ [mNativeMenuItem setImage:nil];
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t origWidth = 0, origHeight = 0;
+ imageContainer->GetWidth(&origWidth);
+ imageContainer->GetHeight(&origHeight);
+
+ // If the image region is invalid, don't draw the image to almost match
+ // the behavior of other platforms.
+ if (!mImageRegionRect.IsEmpty() &&
+ (mImageRegionRect.XMost() > origWidth ||
+ mImageRegionRect.YMost() > origHeight)) {
+ [mNativeMenuItem setImage:nil];
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mImageRegionRect.IsEmpty()) {
+ mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
+ }
+
+ RefPtr<SourceSurface> surface =
+ imageContainer->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+ if (!surface) {
+ [mNativeMenuItem setImage:nil];
+ return NS_ERROR_FAILURE;
+ }
+
+ CGImageRef origImage = NULL;
+ nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &origImage);
+ if (NS_FAILED(rv) || !origImage) {
+ [mNativeMenuItem setImage:nil];
+ return NS_ERROR_FAILURE;
+ }
+
+ bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
+ mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
+
+ CGImageRef finalImage = origImage;
+ if (createSubImage) {
+ // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall
+ // image to use as the icon
+ finalImage = ::CGImageCreateWithImageInRect(origImage,
+ ::CGRectMake(mImageRegionRect.x,
+ mImageRegionRect.y,
+ mImageRegionRect.width,
+ mImageRegionRect.height));
+ ::CGImageRelease(origImage);
+ if (!finalImage) {
+ [mNativeMenuItem setImage:nil];
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ NSImage *newImage = nil;
+ rv = nsCocoaUtils::CreateNSImageFromCGImage(finalImage, &newImage);
+ if (NS_FAILED(rv) || !newImage) {
+ [mNativeMenuItem setImage:nil];
+ ::CGImageRelease(finalImage);
+ return NS_ERROR_FAILURE;
+ }
+
+ [newImage setSize:NSMakeSize(kIconWidth, kIconHeight)];
+ [mNativeMenuItem setImage:newImage];
+
+ [newImage release];
+ ::CGImageRelease(finalImage);
+
+ mLoadedIcon = true;
+ mSetIcon = true;
+
+ if (mMenuObject) {
+ mMenuObject->IconUpdated();
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}