1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
15 * The Original Code is support for icons in native menu items on Mac OS X.
17 * The Initial Developer of the Original Code is Google Inc.
18 * Portions created by the Initial Developer are Copyright (C) 2006
19 * the Initial Developer. All Rights Reserved.
22 * Mark Mentovai <mark@moxienet.com> (Original Author)
23 * Josh Aas <josh@mozilla.com>
24 * Benjamin Frisch <bfrisch@gmail.com>
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
38 * ***** END LICENSE BLOCK ***** */
41 * Retrieves and displays icons in native menu items on Mac OS X.
44 #include "nsMenuItemIconX.h"
46 #include "nsObjCExceptions.h"
48 #include "nsIContent.h"
49 #include "nsIDocument.h"
50 #include "nsINameSpaceManager.h"
51 #include "nsWidgetAtoms.h"
52 #include "nsIDOMDocumentView.h"
53 #include "nsIDOMViewCSS.h"
54 #include "nsIDOMElement.h"
55 #include "nsIDOMCSSStyleDeclaration.h"
56 #include "nsIDOMCSSValue.h"
57 #include "nsIDOMCSSPrimitiveValue.h"
58 #include "nsIDOMRect.h"
59 #include "nsThreadUtils.h"
60 #include "nsToolkit.h"
61 #include "nsNetUtil.h"
62 #include "imgILoader.h"
63 #include "imgIRequest.h"
64 #include "nsMenuItemX.h"
65 #include "gfxImageSurface.h"
66 #include "imgIContainer.h"
67 #include "nsCocoaUtils.h"
69 static const PRUint32 kIconWidth = 16;
70 static const PRUint32 kIconHeight = 16;
71 static const PRUint32 kIconBitsPerComponent = 8;
72 static const PRUint32 kIconComponents = 4;
73 static const PRUint32 kIconBitsPerPixel = kIconBitsPerComponent *
75 static const PRUint32 kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8;
76 static const PRUint32 kIconBytes = kIconBytesPerRow * kIconHeight;
78 typedef nsresult (nsIDOMRect::*GetRectSideMethod)(nsIDOMCSSPrimitiveValue**);
80 NS_IMPL_ISUPPORTS2(nsMenuItemIconX, imgIContainerObserver, imgIDecoderObserver)
82 nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem,
84 NSMenuItem* aNativeMenuItem)
86 , mMenuObject(aMenuItem)
87 , mLoadedIcon(PR_FALSE)
89 , mNativeMenuItem(aNativeMenuItem)
91 // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem);
94 nsMenuItemIconX::~nsMenuItemIconX()
97 mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
100 // Called from mMenuObjectX's destructor, to prevent us from outliving it
101 // (as might otherwise happen if calls to our imgIDecoderObserver methods
102 // are still outstanding). mMenuObjectX owns our nNativeMenuItem.
103 void nsMenuItemIconX::Destroy()
106 mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
107 mIconRequest = nsnull;
109 mMenuObject = nsnull;
110 mNativeMenuItem = nil;
114 nsMenuItemIconX::SetupIcon()
116 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
118 // Still don't have one, then something is wrong, get out of here.
119 if (!mNativeMenuItem) {
120 NS_ERROR("No native menu item");
121 return NS_ERROR_FAILURE;
124 nsCOMPtr<nsIURI> iconURI;
125 nsresult rv = GetIconURI(getter_AddRefs(iconURI));
127 // There is no icon for this menu item. An icon might have been set
128 // earlier. Clear it.
129 [mNativeMenuItem setImage:nil];
134 rv = LoadIcon(iconURI);
136 // There is no icon for this menu item, as an error occurred while loading it.
137 // An icon might have been set earlier or the place holder icon may have
138 // been set. Clear it.
139 [mNativeMenuItem setImage:nil];
143 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
147 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
149 nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
150 (aRect->*aMethod)(getter_AddRefs(dimensionValue));
154 PRUint16 primitiveType;
155 nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
156 if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
160 rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
165 return NSToIntRound(dimension);
169 nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
172 return NS_ERROR_FAILURE;
174 // Mac native menu items support having both a checkmark and an icon
175 // simultaneously, but this is unheard of in the cross-platform toolkit,
176 // seemingly because the win32 theme is unable to cope with both at once.
177 // The downside is that it's possible to get a menu item marked with a
178 // native checkmark and a checkmark for an icon. Head off that possibility
179 // by pretending that no icon exists if this is a checkable menu item.
180 if (mMenuObject->MenuObjectType() == eMenuItemObjectType) {
181 nsMenuItemX* menuItem = static_cast<nsMenuItemX*>(mMenuObject);
182 if (menuItem->GetMenuItemType() != eRegularMenuItemType)
183 return NS_ERROR_FAILURE;
187 return NS_ERROR_FAILURE;
189 // First, look at the content node's "image" attribute.
190 nsAutoString imageURIString;
191 PRBool hasImageAttr = mContent->GetAttr(kNameSpaceID_None,
192 nsWidgetAtoms::image,
196 nsCOMPtr<nsIDOMCSSValue> cssValue;
197 nsCOMPtr<nsIDOMCSSStyleDeclaration> cssStyleDecl;
198 nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
199 PRUint16 primitiveType;
201 // If the content node has no "image" attribute, get the
202 // "list-style-image" property from CSS.
203 nsCOMPtr<nsIDOMDocumentView> domDocumentView =
204 do_QueryInterface(mContent->GetDocument());
205 if (!domDocumentView) return NS_ERROR_FAILURE;
207 nsCOMPtr<nsIDOMAbstractView> domAbstractView;
208 rv = domDocumentView->GetDefaultView(getter_AddRefs(domAbstractView));
209 if (NS_FAILED(rv)) return rv;
211 nsCOMPtr<nsIDOMViewCSS> domViewCSS = do_QueryInterface(domAbstractView);
212 if (!domViewCSS) return NS_ERROR_FAILURE;
214 nsCOMPtr<nsIDOMElement> domElement = do_QueryInterface(mContent);
215 if (!domElement) return NS_ERROR_FAILURE;
218 rv = domViewCSS->GetComputedStyle(domElement, EmptyString(),
219 getter_AddRefs(cssStyleDecl));
220 if (NS_FAILED(rv)) return rv;
222 NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image");
223 rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage,
224 getter_AddRefs(cssValue));
225 if (NS_FAILED(rv)) return rv;
227 primitiveValue = do_QueryInterface(cssValue);
228 if (!primitiveValue) return NS_ERROR_FAILURE;
230 rv = primitiveValue->GetPrimitiveType(&primitiveType);
231 if (NS_FAILED(rv)) return rv;
232 if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI)
233 return NS_ERROR_FAILURE;
235 rv = primitiveValue->GetStringValue(imageURIString);
236 if (NS_FAILED(rv)) return rv;
239 // Empty the mImageRegionRect initially as the image region CSS could
240 // have been changed and now have an error or have been removed since the
241 // last GetIconURI call.
242 mImageRegionRect.Empty();
244 // If this menu item shouldn't have an icon, the string will be empty,
245 // and NS_NewURI will fail.
246 nsCOMPtr<nsIURI> iconURI;
247 rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
248 if (NS_FAILED(rv)) return rv;
251 NS_ADDREF(*aIconURI);
254 // Check if the icon has a specified image region so that it can be
255 // cropped appropriately before being displayed.
256 NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region");
257 rv = cssStyleDecl->GetPropertyCSSValue(imageRegion,
258 getter_AddRefs(cssValue));
259 // Just return NS_OK if there if there is a failure due to no
260 // moz-image region specified so the whole icon will be drawn anyway.
261 if (NS_FAILED(rv)) return NS_OK;
263 primitiveValue = do_QueryInterface(cssValue);
264 if (!primitiveValue) return NS_OK;
266 rv = primitiveValue->GetPrimitiveType(&primitiveType);
267 if (NS_FAILED(rv)) return NS_OK;
268 if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT)
271 nsCOMPtr<nsIDOMRect> imageRegionRect;
272 rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect));
273 if (NS_FAILED(rv)) return NS_OK;
275 if (imageRegionRect) {
276 // Return NS_ERROR_FAILURE if the image region is invalid so the image
277 // is not drawn, and behavior is similar to XUL menus.
278 PRInt32 bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom);
279 PRInt32 right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight);
280 PRInt32 top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop);
281 PRInt32 left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft);
283 if (top < 0 || left < 0 || bottom <= top || right <= left)
284 return NS_ERROR_FAILURE;
286 mImageRegionRect.SetRect(left, top, right - left, bottom - top);
294 nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
296 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
299 // Another icon request is already in flight. Kill it.
300 mIconRequest->Cancel(NS_BINDING_ABORTED);
301 mIconRequest = nsnull;
304 mLoadedIcon = PR_FALSE;
306 if (!mContent) return NS_ERROR_FAILURE;
308 nsCOMPtr<nsIDocument> document = mContent->GetOwnerDoc();
309 if (!document) return NS_ERROR_FAILURE;
311 nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup();
312 if (!loadGroup) return NS_ERROR_FAILURE;
314 nsresult rv = NS_ERROR_FAILURE;
315 nsCOMPtr<imgILoader> loader = do_GetService("@mozilla.org/image/loader;1",
317 if (NS_FAILED(rv)) return rv;
320 // Set a completely transparent 16x16 image as the icon on this menu item
321 // as a placeholder. This keeps the menu item text displayed in the same
322 // position that it will be displayed when the real icon is loaded, and
323 // prevents it from jumping around or looking misaligned.
325 static PRBool sInitializedPlaceholder;
326 static NSImage* sPlaceholderIconImage;
327 if (!sInitializedPlaceholder) {
328 sInitializedPlaceholder = PR_TRUE;
330 // Note that we only create the one and reuse it forever, so this is not a leak.
331 sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)];
334 if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
337 [mNativeMenuItem setImage:sPlaceholderIconImage];
340 // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is
341 // not exposed to web content
342 rv = loader->LoadImage(aIconURI, nsnull, nsnull, loadGroup, this,
343 nsnull, nsIRequest::LOAD_NORMAL, nsnull, nsnull,
344 nsnull, getter_AddRefs(mIconRequest));
345 if (NS_FAILED(rv)) return rv;
349 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
353 // imgIContainerObserver
357 nsMenuItemIconX::FrameChanged(imgIContainer* aContainer,
358 const nsIntRect* aDirtyRect)
364 // imgIDecoderObserver
368 nsMenuItemIconX::OnStartRequest(imgIRequest* aRequest)
374 nsMenuItemIconX::OnStartDecode(imgIRequest* aRequest)
380 nsMenuItemIconX::OnStartContainer(imgIRequest* aRequest,
381 imgIContainer* aContainer)
384 NS_ABORT_IF_FALSE(aContainer, "who sent the notification then?");
385 aContainer->RequestDecode();
391 nsMenuItemIconX::OnStartFrame(imgIRequest* aRequest, PRUint32 aFrame)
397 nsMenuItemIconX::OnDataAvailable(imgIRequest* aRequest,
398 PRBool aCurrentFrame,
399 const nsIntRect* aRect)
405 nsMenuItemIconX::OnStopFrame(imgIRequest* aRequest,
408 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
410 if (aRequest != mIconRequest)
411 return NS_ERROR_FAILURE;
413 // Only support one frame.
417 if (!mNativeMenuItem)
418 return NS_ERROR_FAILURE;
420 nsCOMPtr<imgIContainer> imageContainer;
421 aRequest->GetImage(getter_AddRefs(imageContainer));
422 if (!imageContainer) {
423 [mNativeMenuItem setImage:nil];
424 return NS_ERROR_FAILURE;
427 PRInt32 origWidth = 0, origHeight = 0;
428 imageContainer->GetWidth(&origWidth);
429 imageContainer->GetHeight(&origHeight);
431 // If the image region is invalid, don't draw the image to almost match
432 // the behavior of other platforms.
433 if (!mImageRegionRect.IsEmpty() &&
434 (mImageRegionRect.XMost() > origWidth ||
435 mImageRegionRect.YMost() > origHeight)) {
436 [mNativeMenuItem setImage:nil];
437 return NS_ERROR_FAILURE;
440 if (mImageRegionRect.IsEmpty()) {
441 mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
444 nsRefPtr<gfxImageSurface> frame;
445 nsresult rv = imageContainer->CopyFrame( imgIContainer::FRAME_CURRENT,
446 imgIContainer::FLAG_NONE,
447 getter_AddRefs(frame));
448 if (NS_FAILED(rv) || !frame) {
449 [mNativeMenuItem setImage:nil];
450 return NS_ERROR_FAILURE;
452 CGImageRef origImage = NULL;
453 rv = nsCocoaUtils::CreateCGImageFromSurface(frame, &origImage);
454 if (NS_FAILED(rv) || !origImage) {
455 [mNativeMenuItem setImage:nil];
456 return NS_ERROR_FAILURE;
459 PRBool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
460 mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
462 CGImageRef finalImage = NULL;
463 if (createSubImage) {
464 // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall
465 // image to use as the icon
466 finalImage = ::CGImageCreateWithImageInRect(origImage,
467 ::CGRectMake(mImageRegionRect.x,
469 mImageRegionRect.width,
470 mImageRegionRect.height));
471 ::CGImageRelease(origImage);
473 [mNativeMenuItem setImage:nil];
474 return NS_ERROR_FAILURE;
477 finalImage = origImage;
479 // The image may not be the right size for a menu icon (16x16).
480 // Create a new CGImage for the menu item.
481 PRUint8* bitmap = (PRUint8*)malloc(kIconBytes);
483 CGColorSpaceRef colorSpace = ::CGColorSpaceCreateDeviceRGB();
485 CGContextRef bitmapContext = ::CGBitmapContextCreate(bitmap, kIconWidth, kIconHeight,
486 kIconBitsPerComponent,
489 kCGImageAlphaPremultipliedLast);
490 ::CGColorSpaceRelease(colorSpace);
491 if (!bitmapContext) {
492 ::CGImageRelease(finalImage);
494 ::CGColorSpaceRelease(colorSpace);
495 return NS_ERROR_FAILURE;
497 CGRect iconRect = ::CGRectMake(0, 0, kIconWidth, kIconHeight);
498 ::CGContextClearRect(bitmapContext, iconRect);
499 ::CGContextDrawImage(bitmapContext, iconRect, finalImage);
501 CGImageRef iconImage = ::CGBitmapContextCreateImage(bitmapContext);
503 ::CGImageRelease(finalImage);
504 ::CGContextRelease(bitmapContext);
507 if (!iconImage) return NS_ERROR_FAILURE;
509 NSImage *newImage = nil;
510 rv = nsCocoaUtils::CreateNSImageFromCGImage(iconImage, &newImage);
511 if (NS_FAILED(rv) || !newImage) {
512 [mNativeMenuItem setImage:nil];
513 ::CGImageRelease(iconImage);
514 return NS_ERROR_FAILURE;
517 [mNativeMenuItem setImage:newImage];
520 ::CGImageRelease(iconImage);
522 mLoadedIcon = PR_TRUE;
527 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
531 nsMenuItemIconX::OnStopContainer(imgIRequest* aRequest,
532 imgIContainer* aContainer)
538 nsMenuItemIconX::OnStopDecode(imgIRequest* aRequest,
540 const PRUnichar* statusArg)
546 nsMenuItemIconX::OnStopRequest(imgIRequest* aRequest,
549 NS_ASSERTION(mIconRequest, "NULL mIconRequest! Multiple calls to OnStopRequest()?");
551 mIconRequest->Cancel(NS_BINDING_ABORTED);
552 mIconRequest = nsnull;
558 nsMenuItemIconX::OnDiscard(imgIRequest* aRequest)