/* -*- 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. */ #include "nsMenuItemIconX.h" #include "nsObjCExceptions.h" #include "prmem.h" #include "nsIContent.h" #include "nsIDocument.h" #include "nsINameSpaceManager.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 "imgILoader.h" #include "imgIRequest.h" #include "nsMenuItemX.h" #include "gfxImageSurface.h" #include "imgIContainer.h" #include "nsCocoaUtils.h" static const PRUint32 kIconWidth = 16; static const PRUint32 kIconHeight = 16; static const PRUint32 kIconBitsPerComponent = 8; static const PRUint32 kIconComponents = 4; static const PRUint32 kIconBitsPerPixel = kIconBitsPerComponent * kIconComponents; static const PRUint32 kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8; static const PRUint32 kIconBytes = kIconBytesPerRow * kIconHeight; typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect, GetBottom, (nsIDOMCSSPrimitiveValue**)); NS_IMPL_ISUPPORTS2(nsMenuItemIconX, imgIContainerObserver, imgIDecoderObserver) 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 imgIDecoderObserver methods // are still outstanding). mMenuObjectX owns our nNativeMenuItem. void nsMenuItemIconX::Destroy() { if (mIconRequest) { mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); mIconRequest = nsnull; } mMenuObject = nsnull; 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 PRInt32 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod) { nsCOMPtr dimensionValue; (aRect->*aMethod)(getter_AddRefs(dimensionValue)); if (!dimensionValue) return -1; PRUint16 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; PRUint16 primitiveType; if (!hasImageAttr) { // If the content node has no "image" attribute, get the // "list-style-image" property from CSS. nsCOMPtr domDocument = do_QueryInterface(mContent->GetDocument()); if (!domDocument) return NS_ERROR_FAILURE; nsCOMPtr window; rv = domDocument->GetDefaultView(getter_AddRefs(window)); if (NS_FAILED(rv)) return rv; 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. PRInt32 bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom); PRInt32 right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight); PRInt32 top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop); PRInt32 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 = nsnull; } mLoadedIcon = false; if (!mContent) return NS_ERROR_FAILURE; nsCOMPtr document = mContent->OwnerDoc(); nsCOMPtr loadGroup = document->GetDocumentLoadGroup(); if (!loadGroup) return NS_ERROR_FAILURE; nsresult rv = NS_ERROR_FAILURE; nsCOMPtr loader = do_GetService("@mozilla.org/image/loader;1", &rv); if (NS_FAILED(rv)) return rv; 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]; } // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is // not exposed to web content rv = loader->LoadImage(aIconURI, nsnull, nsnull, nsnull, loadGroup, this, nsnull, nsIRequest::LOAD_NORMAL, nsnull, nsnull, nsnull, getter_AddRefs(mIconRequest)); if (NS_FAILED(rv)) return rv; // We need to request the icon be decoded (bug 573583, bug 705516). mIconRequest->RequestDecode(); return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // // imgIContainerObserver // NS_IMETHODIMP nsMenuItemIconX::FrameChanged(imgIRequest* aRequest, imgIContainer* aContainer, const nsIntRect* aDirtyRect) { return NS_OK; } // // imgIDecoderObserver // NS_IMETHODIMP nsMenuItemIconX::OnStartRequest(imgIRequest* aRequest) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStartDecode(imgIRequest* aRequest) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStartContainer(imgIRequest* aRequest, imgIContainer* aContainer) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStartFrame(imgIRequest* aRequest, PRUint32 aFrame) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnDataAvailable(imgIRequest* aRequest, bool aCurrentFrame, const nsIntRect* aRect) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStopFrame(imgIRequest* aRequest, PRUint32 aFrame) { 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; } PRInt32 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); } nsRefPtr frame; nsresult rv = imageContainer->CopyFrame( imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_NONE, getter_AddRefs(frame)); if (NS_FAILED(rv) || !frame) { [mNativeMenuItem setImage:nil]; return NS_ERROR_FAILURE; } CGImageRef origImage = NULL; rv = nsCocoaUtils::CreateCGImageFromSurface(frame, &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. PRUint8* bitmap = (PRUint8*)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; } NS_IMETHODIMP nsMenuItemIconX::OnStopContainer(imgIRequest* aRequest, imgIContainer* aContainer) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStopDecode(imgIRequest* aRequest, nsresult status, const PRUnichar* statusArg) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnStopRequest(imgIRequest* aRequest, bool aIsLastPart) { if (mIconRequest && mIconRequest == aRequest) { mIconRequest->Cancel(NS_BINDING_ABORTED); mIconRequest = nsnull; } return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnDiscard(imgIRequest* aRequest) { return NS_OK; } NS_IMETHODIMP nsMenuItemIconX::OnImageIsAnimated(imgIRequest* aRequest) { return NS_OK; }