michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * Retrieves and displays icons in native menu items on Mac OS X. michael@0: */ michael@0: michael@0: /* exception_defines.h defines 'try' to 'if (true)' which breaks objective-c michael@0: exceptions and produces errors like: error: unexpected '@' in program'. michael@0: If we define __EXCEPTIONS exception_defines.h will avoid doing this. michael@0: michael@0: See bug 666609 for more information. michael@0: michael@0: We use to get the libstdc++ version. */ michael@0: #include michael@0: #if __GLIBCXX__ <= 20070719 michael@0: #define __EXCEPTIONS michael@0: #endif michael@0: michael@0: #include "nsMenuItemIconX.h" michael@0: #include "nsObjCExceptions.h" michael@0: #include "nsIContent.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsNameSpaceManager.h" michael@0: #include "nsGkAtoms.h" michael@0: #include "nsIDOMElement.h" michael@0: #include "nsIDOMCSSStyleDeclaration.h" michael@0: #include "nsIDOMCSSValue.h" michael@0: #include "nsIDOMCSSPrimitiveValue.h" michael@0: #include "nsIDOMRect.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "nsToolkit.h" michael@0: #include "nsNetUtil.h" michael@0: #include "imgLoader.h" michael@0: #include "imgRequestProxy.h" michael@0: #include "nsMenuItemX.h" michael@0: #include "gfxPlatform.h" michael@0: #include "imgIContainer.h" michael@0: #include "nsCocoaUtils.h" michael@0: #include "mozIThirdPartyUtil.h" michael@0: #include "nsContentUtils.h" michael@0: michael@0: using mozilla::gfx::SourceSurface; michael@0: using mozilla::RefPtr; michael@0: michael@0: static const uint32_t kIconWidth = 16; michael@0: static const uint32_t kIconHeight = 16; michael@0: static const uint32_t kIconBitsPerComponent = 8; michael@0: static const uint32_t kIconComponents = 4; michael@0: static const uint32_t kIconBitsPerPixel = kIconBitsPerComponent * michael@0: kIconComponents; michael@0: static const uint32_t kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8; michael@0: static const uint32_t kIconBytes = kIconBytesPerRow * kIconHeight; michael@0: michael@0: typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect, michael@0: GetBottom, (nsIDOMCSSPrimitiveValue**)); michael@0: michael@0: NS_IMPL_ISUPPORTS(nsMenuItemIconX, imgINotificationObserver) michael@0: michael@0: nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem, michael@0: nsIContent* aContent, michael@0: NSMenuItem* aNativeMenuItem) michael@0: : mContent(aContent) michael@0: , mMenuObject(aMenuItem) michael@0: , mLoadedIcon(false) michael@0: , mSetIcon(false) michael@0: , mNativeMenuItem(aNativeMenuItem) michael@0: { michael@0: // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem); michael@0: } michael@0: michael@0: nsMenuItemIconX::~nsMenuItemIconX() michael@0: { michael@0: if (mIconRequest) michael@0: mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); michael@0: } michael@0: michael@0: // Called from mMenuObjectX's destructor, to prevent us from outliving it michael@0: // (as might otherwise happen if calls to our imgINotificationObserver methods michael@0: // are still outstanding). mMenuObjectX owns our nNativeMenuItem. michael@0: void nsMenuItemIconX::Destroy() michael@0: { michael@0: if (mIconRequest) { michael@0: mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); michael@0: mIconRequest = nullptr; michael@0: } michael@0: mMenuObject = nullptr; michael@0: mNativeMenuItem = nil; michael@0: } michael@0: michael@0: nsresult michael@0: nsMenuItemIconX::SetupIcon() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: // Still don't have one, then something is wrong, get out of here. michael@0: if (!mNativeMenuItem) { michael@0: NS_ERROR("No native menu item"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: nsCOMPtr iconURI; michael@0: nsresult rv = GetIconURI(getter_AddRefs(iconURI)); michael@0: if (NS_FAILED(rv)) { michael@0: // There is no icon for this menu item. An icon might have been set michael@0: // earlier. Clear it. michael@0: [mNativeMenuItem setImage:nil]; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: rv = LoadIcon(iconURI); michael@0: if (NS_FAILED(rv)) { michael@0: // There is no icon for this menu item, as an error occurred while loading it. michael@0: // An icon might have been set earlier or the place holder icon may have michael@0: // been set. Clear it. michael@0: [mNativeMenuItem setImage:nil]; michael@0: } michael@0: return rv; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: static int32_t michael@0: GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod) michael@0: { michael@0: nsCOMPtr dimensionValue; michael@0: (aRect->*aMethod)(getter_AddRefs(dimensionValue)); michael@0: if (!dimensionValue) michael@0: return -1; michael@0: michael@0: uint16_t primitiveType; michael@0: nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType); michael@0: if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX) michael@0: return -1; michael@0: michael@0: float dimension = 0; michael@0: rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX, michael@0: &dimension); michael@0: if (NS_FAILED(rv)) michael@0: return -1; michael@0: michael@0: return NSToIntRound(dimension); michael@0: } michael@0: michael@0: nsresult michael@0: nsMenuItemIconX::GetIconURI(nsIURI** aIconURI) michael@0: { michael@0: if (!mMenuObject) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: // Mac native menu items support having both a checkmark and an icon michael@0: // simultaneously, but this is unheard of in the cross-platform toolkit, michael@0: // seemingly because the win32 theme is unable to cope with both at once. michael@0: // The downside is that it's possible to get a menu item marked with a michael@0: // native checkmark and a checkmark for an icon. Head off that possibility michael@0: // by pretending that no icon exists if this is a checkable menu item. michael@0: if (mMenuObject->MenuObjectType() == eMenuItemObjectType) { michael@0: nsMenuItemX* menuItem = static_cast(mMenuObject); michael@0: if (menuItem->GetMenuItemType() != eRegularMenuItemType) michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: if (!mContent) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: // First, look at the content node's "image" attribute. michael@0: nsAutoString imageURIString; michael@0: bool hasImageAttr = mContent->GetAttr(kNameSpaceID_None, michael@0: nsGkAtoms::image, michael@0: imageURIString); michael@0: michael@0: nsresult rv; michael@0: nsCOMPtr cssValue; michael@0: nsCOMPtr cssStyleDecl; michael@0: nsCOMPtr primitiveValue; michael@0: uint16_t primitiveType; michael@0: if (!hasImageAttr) { michael@0: // If the content node has no "image" attribute, get the michael@0: // "list-style-image" property from CSS. michael@0: nsCOMPtr document = mContent->GetDocument(); michael@0: if (!document) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: nsCOMPtr window = document->GetWindow(); michael@0: if (!window) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: nsCOMPtr domElement = do_QueryInterface(mContent); michael@0: if (!domElement) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: michael@0: rv = window->GetComputedStyle(domElement, EmptyString(), michael@0: getter_AddRefs(cssStyleDecl)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image"); michael@0: rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage, michael@0: getter_AddRefs(cssValue)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: primitiveValue = do_QueryInterface(cssValue); michael@0: if (!primitiveValue) return NS_ERROR_FAILURE; michael@0: michael@0: rv = primitiveValue->GetPrimitiveType(&primitiveType); michael@0: if (NS_FAILED(rv)) return rv; michael@0: if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: rv = primitiveValue->GetStringValue(imageURIString); michael@0: if (NS_FAILED(rv)) return rv; michael@0: } michael@0: michael@0: // Empty the mImageRegionRect initially as the image region CSS could michael@0: // have been changed and now have an error or have been removed since the michael@0: // last GetIconURI call. michael@0: mImageRegionRect.SetEmpty(); michael@0: michael@0: // If this menu item shouldn't have an icon, the string will be empty, michael@0: // and NS_NewURI will fail. michael@0: nsCOMPtr iconURI; michael@0: rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: *aIconURI = iconURI; michael@0: NS_ADDREF(*aIconURI); michael@0: michael@0: if (!hasImageAttr) { michael@0: // Check if the icon has a specified image region so that it can be michael@0: // cropped appropriately before being displayed. michael@0: NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region"); michael@0: rv = cssStyleDecl->GetPropertyCSSValue(imageRegion, michael@0: getter_AddRefs(cssValue)); michael@0: // Just return NS_OK if there if there is a failure due to no michael@0: // moz-image region specified so the whole icon will be drawn anyway. michael@0: if (NS_FAILED(rv)) return NS_OK; michael@0: michael@0: primitiveValue = do_QueryInterface(cssValue); michael@0: if (!primitiveValue) return NS_OK; michael@0: michael@0: rv = primitiveValue->GetPrimitiveType(&primitiveType); michael@0: if (NS_FAILED(rv)) return NS_OK; michael@0: if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr imageRegionRect; michael@0: rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect)); michael@0: if (NS_FAILED(rv)) return NS_OK; michael@0: michael@0: if (imageRegionRect) { michael@0: // Return NS_ERROR_FAILURE if the image region is invalid so the image michael@0: // is not drawn, and behavior is similar to XUL menus. michael@0: int32_t bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom); michael@0: int32_t right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight); michael@0: int32_t top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop); michael@0: int32_t left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft); michael@0: michael@0: if (top < 0 || left < 0 || bottom <= top || right <= left) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: mImageRegionRect.SetRect(left, top, right - left, bottom - top); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsMenuItemIconX::LoadIcon(nsIURI* aIconURI) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (mIconRequest) { michael@0: // Another icon request is already in flight. Kill it. michael@0: mIconRequest->Cancel(NS_BINDING_ABORTED); michael@0: mIconRequest = nullptr; michael@0: } michael@0: michael@0: mLoadedIcon = false; michael@0: michael@0: if (!mContent) return NS_ERROR_FAILURE; michael@0: michael@0: nsCOMPtr document = mContent->OwnerDoc(); michael@0: michael@0: nsCOMPtr loadGroup = document->GetDocumentLoadGroup(); michael@0: if (!loadGroup) return NS_ERROR_FAILURE; michael@0: michael@0: nsRefPtr loader = nsContentUtils::GetImgLoaderForDocument(document); michael@0: if (!loader) return NS_ERROR_FAILURE; michael@0: michael@0: if (!mSetIcon) { michael@0: // Set a completely transparent 16x16 image as the icon on this menu item michael@0: // as a placeholder. This keeps the menu item text displayed in the same michael@0: // position that it will be displayed when the real icon is loaded, and michael@0: // prevents it from jumping around or looking misaligned. michael@0: michael@0: static bool sInitializedPlaceholder; michael@0: static NSImage* sPlaceholderIconImage; michael@0: if (!sInitializedPlaceholder) { michael@0: sInitializedPlaceholder = true; michael@0: michael@0: // Note that we only create the one and reuse it forever, so this is not a leak. michael@0: sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)]; michael@0: } michael@0: michael@0: if (!sPlaceholderIconImage) return NS_ERROR_FAILURE; michael@0: michael@0: if (mNativeMenuItem) michael@0: [mNativeMenuItem setImage:sPlaceholderIconImage]; michael@0: } michael@0: michael@0: nsCOMPtr firstPartyIsolationURI; michael@0: nsCOMPtr thirdPartySvc michael@0: = do_GetService(THIRDPARTYUTIL_CONTRACTID); michael@0: thirdPartySvc->GetFirstPartyURI(nullptr, document, michael@0: getter_AddRefs(firstPartyIsolationURI)); michael@0: michael@0: // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is michael@0: // not exposed to web content michael@0: nsresult rv = loader->LoadImage(aIconURI, firstPartyIsolationURI, nullptr, nullptr, loadGroup, this, michael@0: nullptr, nsIRequest::LOAD_NORMAL, nullptr, michael@0: nullptr, EmptyString(), getter_AddRefs(mIconRequest)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: // We need to request the icon be decoded (bug 573583, bug 705516). michael@0: mIconRequest->StartDecoding(); michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: // michael@0: // imgINotificationObserver michael@0: // michael@0: michael@0: NS_IMETHODIMP michael@0: nsMenuItemIconX::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData) michael@0: { michael@0: if (aType == imgINotificationObserver::FRAME_COMPLETE) { michael@0: return OnStopFrame(aRequest); michael@0: } michael@0: michael@0: if (aType == imgINotificationObserver::DECODE_COMPLETE) { michael@0: if (mIconRequest && mIconRequest == aRequest) { michael@0: mIconRequest->Cancel(NS_BINDING_ABORTED); michael@0: mIconRequest = nullptr; michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsMenuItemIconX::OnStopFrame(imgIRequest* aRequest) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (aRequest != mIconRequest) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: // Only support one frame. michael@0: if (mLoadedIcon) michael@0: return NS_OK; michael@0: michael@0: if (!mNativeMenuItem) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: nsCOMPtr imageContainer; michael@0: aRequest->GetImage(getter_AddRefs(imageContainer)); michael@0: if (!imageContainer) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: int32_t origWidth = 0, origHeight = 0; michael@0: imageContainer->GetWidth(&origWidth); michael@0: imageContainer->GetHeight(&origHeight); michael@0: michael@0: // If the image region is invalid, don't draw the image to almost match michael@0: // the behavior of other platforms. michael@0: if (!mImageRegionRect.IsEmpty() && michael@0: (mImageRegionRect.XMost() > origWidth || michael@0: mImageRegionRect.YMost() > origHeight)) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: if (mImageRegionRect.IsEmpty()) { michael@0: mImageRegionRect.SetRect(0, 0, origWidth, origHeight); michael@0: } michael@0: michael@0: RefPtr surface = michael@0: imageContainer->GetFrame(imgIContainer::FRAME_CURRENT, michael@0: imgIContainer::FLAG_NONE); michael@0: if (!surface) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: CGImageRef origImage = NULL; michael@0: nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &origImage); michael@0: if (NS_FAILED(rv) || !origImage) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 && michael@0: mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight); michael@0: michael@0: CGImageRef finalImage = NULL; michael@0: if (createSubImage) { michael@0: // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall michael@0: // image to use as the icon michael@0: finalImage = ::CGImageCreateWithImageInRect(origImage, michael@0: ::CGRectMake(mImageRegionRect.x, michael@0: mImageRegionRect.y, michael@0: mImageRegionRect.width, michael@0: mImageRegionRect.height)); michael@0: ::CGImageRelease(origImage); michael@0: if (!finalImage) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } else { michael@0: finalImage = origImage; michael@0: } michael@0: // The image may not be the right size for a menu icon (16x16). michael@0: // Create a new CGImage for the menu item. michael@0: uint8_t* bitmap = (uint8_t*)malloc(kIconBytes); michael@0: michael@0: CGColorSpaceRef colorSpace = ::CGColorSpaceCreateDeviceRGB(); michael@0: michael@0: CGContextRef bitmapContext = ::CGBitmapContextCreate(bitmap, kIconWidth, kIconHeight, michael@0: kIconBitsPerComponent, michael@0: kIconBytesPerRow, michael@0: colorSpace, michael@0: kCGImageAlphaPremultipliedLast); michael@0: ::CGColorSpaceRelease(colorSpace); michael@0: if (!bitmapContext) { michael@0: ::CGImageRelease(finalImage); michael@0: free(bitmap); michael@0: ::CGColorSpaceRelease(colorSpace); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: CGRect iconRect = ::CGRectMake(0, 0, kIconWidth, kIconHeight); michael@0: ::CGContextClearRect(bitmapContext, iconRect); michael@0: ::CGContextDrawImage(bitmapContext, iconRect, finalImage); michael@0: michael@0: CGImageRef iconImage = ::CGBitmapContextCreateImage(bitmapContext); michael@0: michael@0: ::CGImageRelease(finalImage); michael@0: ::CGContextRelease(bitmapContext); michael@0: free(bitmap); michael@0: michael@0: if (!iconImage) return NS_ERROR_FAILURE; michael@0: michael@0: NSImage *newImage = nil; michael@0: rv = nsCocoaUtils::CreateNSImageFromCGImage(iconImage, &newImage); michael@0: if (NS_FAILED(rv) || !newImage) { michael@0: [mNativeMenuItem setImage:nil]; michael@0: ::CGImageRelease(iconImage); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: [mNativeMenuItem setImage:newImage]; michael@0: michael@0: [newImage release]; michael@0: ::CGImageRelease(iconImage); michael@0: michael@0: mLoadedIcon = true; michael@0: mSetIcon = true; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: }