diff -r 000000000000 -r 6474c204b198 widget/cocoa/nsMenuItemIconX.mm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/widget/cocoa/nsMenuItemIconX.mm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,481 @@ +/* -*- 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 to get the libstdc++ version. */ +#include +#if __GLIBCXX__ <= 20070719 +#define __EXCEPTIONS +#endif + +#include "nsMenuItemIconX.h" +#include "nsObjCExceptions.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsIDOMElement.h" +#include "nsIDOMCSSStyleDeclaration.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 "mozIThirdPartyUtil.h" +#include "nsContentUtils.h" + +using mozilla::gfx::SourceSurface; +using mozilla::RefPtr; + +static const uint32_t kIconWidth = 16; +static const uint32_t kIconHeight = 16; +static const uint32_t kIconBitsPerComponent = 8; +static const uint32_t kIconComponents = 4; +static const uint32_t kIconBitsPerPixel = kIconBitsPerComponent * + kIconComponents; +static const uint32_t kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8; +static const uint32_t kIconBytes = kIconBytesPerRow * kIconHeight; + +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 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 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(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 cssValue; + nsCOMPtr cssStyleDecl; + nsCOMPtr primitiveValue; + uint16_t primitiveType; + if (!hasImageAttr) { + // If the content node has no "image" attribute, get the + // "list-style-image" property from CSS. + nsCOMPtr document = mContent->GetDocument(); + if (!document) + return NS_ERROR_FAILURE; + + nsCOMPtr window = document->GetWindow(); + if (!window) + return NS_ERROR_FAILURE; + + nsCOMPtr domElement = do_QueryInterface(mContent); + if (!domElement) + return NS_ERROR_FAILURE; + + + rv = window->GetComputedStyle(domElement, EmptyString(), + getter_AddRefs(cssStyleDecl)); + if (NS_FAILED(rv)) + return rv; + + 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 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 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 document = mContent->OwnerDoc(); + + nsCOMPtr loadGroup = document->GetDocumentLoadGroup(); + if (!loadGroup) return NS_ERROR_FAILURE; + + nsRefPtr 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]; + } + + nsCOMPtr firstPartyIsolationURI; + nsCOMPtr thirdPartySvc + = do_GetService(THIRDPARTYUTIL_CONTRACTID); + thirdPartySvc->GetFirstPartyURI(nullptr, document, + getter_AddRefs(firstPartyIsolationURI)); + + // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is + // not exposed to web content + nsresult rv = loader->LoadImage(aIconURI, firstPartyIsolationURI, nullptr, nullptr, loadGroup, this, + nullptr, nsIRequest::LOAD_NORMAL, nullptr, + nullptr, EmptyString(), getter_AddRefs(mIconRequest)); + if (NS_FAILED(rv)) return rv; + + // We need to request the icon be decoded (bug 573583, bug 705516). + mIconRequest->StartDecoding(); + + 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::FRAME_COMPLETE) { + return OnStopFrame(aRequest); + } + + if (aType == imgINotificationObserver::DECODE_COMPLETE) { + if (mIconRequest && mIconRequest == aRequest) { + mIconRequest->Cancel(NS_BINDING_ABORTED); + mIconRequest = nullptr; + } + } + + return NS_OK; +} + +nsresult +nsMenuItemIconX::OnStopFrame(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 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 surface = + imageContainer->GetFrame(imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_NONE); + 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 = NULL; + 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; + } + } else { + finalImage = origImage; + } + // The image may not be the right size for a menu icon (16x16). + // Create a new CGImage for the menu item. + uint8_t* bitmap = (uint8_t*)malloc(kIconBytes); + + CGColorSpaceRef colorSpace = ::CGColorSpaceCreateDeviceRGB(); + + CGContextRef bitmapContext = ::CGBitmapContextCreate(bitmap, kIconWidth, kIconHeight, + kIconBitsPerComponent, + kIconBytesPerRow, + colorSpace, + kCGImageAlphaPremultipliedLast); + ::CGColorSpaceRelease(colorSpace); + if (!bitmapContext) { + ::CGImageRelease(finalImage); + free(bitmap); + ::CGColorSpaceRelease(colorSpace); + return NS_ERROR_FAILURE; + } + CGRect iconRect = ::CGRectMake(0, 0, kIconWidth, kIconHeight); + ::CGContextClearRect(bitmapContext, iconRect); + ::CGContextDrawImage(bitmapContext, iconRect, finalImage); + + CGImageRef iconImage = ::CGBitmapContextCreateImage(bitmapContext); + + ::CGImageRelease(finalImage); + ::CGContextRelease(bitmapContext); + free(bitmap); + + if (!iconImage) return NS_ERROR_FAILURE; + + NSImage *newImage = nil; + rv = nsCocoaUtils::CreateNSImageFromCGImage(iconImage, &newImage); + if (NS_FAILED(rv) || !newImage) { + [mNativeMenuItem setImage:nil]; + ::CGImageRelease(iconImage); + return NS_ERROR_FAILURE; + } + + [mNativeMenuItem setImage:newImage]; + + [newImage release]; + ::CGImageRelease(iconImage); + + mLoadedIcon = true; + mSetIcon = true; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +}