CLOSED TREE: TraceMonkey merge head. (a=blockers)
[mozilla-central.git] / widget / src / cocoa / nsMenuItemIconX.mm
blob63c1a011f613da4083e45fe5cdcbef9c1ecb48ee
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
4  *
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/
9  *
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
13  * License.
14  *
15  * The Original Code is support for icons in native menu items on Mac OS X.
16  *
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.
20  *
21  * Contributor(s):
22  *  Mark Mentovai <mark@moxienet.com> (Original Author)
23  *  Josh Aas <josh@mozilla.com>
24  *  Benjamin Frisch <bfrisch@gmail.com>
25  *
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.
37  *
38  * ***** END LICENSE BLOCK ***** */
41  * Retrieves and displays icons in native menu items on Mac OS X.
42  */
44 #include "nsMenuItemIconX.h"
46 #include "nsObjCExceptions.h"
47 #include "prmem.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 *
74                                           kIconComponents;
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,
83                                  nsIContent*    aContent,
84                                  NSMenuItem*    aNativeMenuItem)
85 : mContent(aContent)
86 , mMenuObject(aMenuItem)
87 , mLoadedIcon(PR_FALSE)
88 , mSetIcon(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()
96   if (mIconRequest)
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()
105   if (mIconRequest) {
106     mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
107     mIconRequest = nsnull;
108   }
109   mMenuObject = nsnull;
110   mNativeMenuItem = nil;
113 nsresult
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;
122   }
124   nsCOMPtr<nsIURI> iconURI;
125   nsresult rv = GetIconURI(getter_AddRefs(iconURI));
126   if (NS_FAILED(rv)) {
127     // There is no icon for this menu item. An icon might have been set
128     // earlier.  Clear it.
129     [mNativeMenuItem setImage:nil];
131     return NS_OK;
132   }
134   rv = LoadIcon(iconURI);
135   if (NS_FAILED(rv)) {
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];
140   }
141   return rv;
143   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
146 static PRInt32
147 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
149   nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
150   (aRect->*aMethod)(getter_AddRefs(dimensionValue));
151   if (!dimensionValue)
152     return -1;
154   PRUint16 primitiveType;
155   nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
156   if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
157     return -1;
159   float dimension = 0;
160   rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
161                                      &dimension);
162   if (NS_FAILED(rv))
163     return -1;
165   return NSToIntRound(dimension);
168 nsresult
169 nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
171   if (!mMenuObject)
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;
184   }
186   if (!mContent)
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,
193                                           imageURIString);
195   nsresult rv;
196   nsCOMPtr<nsIDOMCSSValue> cssValue;
197   nsCOMPtr<nsIDOMCSSStyleDeclaration> cssStyleDecl;
198   nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
199   PRUint16 primitiveType;
200   if (!hasImageAttr) {
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;
237   }
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;
250   *aIconURI = iconURI;
251   NS_ADDREF(*aIconURI);
253   if (!hasImageAttr) {
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)
269       return NS_OK;
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);
287     }
288   }
290   return NS_OK;
293 nsresult
294 nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
296   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
298   if (mIconRequest) {
299     // Another icon request is already in flight.  Kill it.
300     mIconRequest->Cancel(NS_BINDING_ABORTED);
301     mIconRequest = nsnull;
302   }
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",
316                                               &rv);
317   if (NS_FAILED(rv)) return rv;
319   if (!mSetIcon) {
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)];
332     }
334     if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
336     if (mNativeMenuItem)
337       [mNativeMenuItem setImage:sPlaceholderIconImage];
338   }
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;
347   return NS_OK;
349   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
353 // imgIContainerObserver
356 NS_IMETHODIMP
357 nsMenuItemIconX::FrameChanged(imgIContainer*   aContainer,
358                               const nsIntRect* aDirtyRect)
360   return NS_OK;
364 // imgIDecoderObserver
367 NS_IMETHODIMP
368 nsMenuItemIconX::OnStartRequest(imgIRequest* aRequest)
370   return NS_OK;
373 NS_IMETHODIMP
374 nsMenuItemIconX::OnStartDecode(imgIRequest* aRequest)
376   return NS_OK;
379 NS_IMETHODIMP
380 nsMenuItemIconX::OnStartContainer(imgIRequest*   aRequest,
381                                   imgIContainer* aContainer)
383   // Request a decode
384   NS_ABORT_IF_FALSE(aContainer, "who sent the notification then?");
385   aContainer->RequestDecode();
387   return NS_OK;
390 NS_IMETHODIMP
391 nsMenuItemIconX::OnStartFrame(imgIRequest* aRequest, PRUint32 aFrame)
393   return NS_OK;
396 NS_IMETHODIMP
397 nsMenuItemIconX::OnDataAvailable(imgIRequest*     aRequest,
398                                  PRBool           aCurrentFrame,
399                                  const nsIntRect* aRect)
401   return NS_OK;
404 NS_IMETHODIMP
405 nsMenuItemIconX::OnStopFrame(imgIRequest*    aRequest,
406                              PRUint32        aFrame)
408   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
410   if (aRequest != mIconRequest)
411     return NS_ERROR_FAILURE;
413   // Only support one frame.
414   if (mLoadedIcon)
415     return NS_OK;
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;
425   }
427   PRInt32 origWidth = 0, origHeight = 0;
428   imageContainer->GetWidth(&origWidth);
429   imageContainer->GetHeight(&origHeight);
430   
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;
438   }
440   if (mImageRegionRect.IsEmpty()) {
441     mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
442   }
443   
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;
451   }      
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;
457   }
459   PRBool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
460                             mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
461   
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, 
468                                                 mImageRegionRect.y,
469                                                 mImageRegionRect.width,
470                                                 mImageRegionRect.height));
471     ::CGImageRelease(origImage);
472     if (!finalImage) {
473       [mNativeMenuItem setImage:nil];
474       return NS_ERROR_FAILURE;  
475     }
476   } else {
477     finalImage = origImage;
478   }
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,
487                                                        kIconBytesPerRow,
488                                                        colorSpace,
489                                                        kCGImageAlphaPremultipliedLast);
490   ::CGColorSpaceRelease(colorSpace);
491   if (!bitmapContext) {
492     ::CGImageRelease(finalImage);
493     free(bitmap);
494     ::CGColorSpaceRelease(colorSpace);
495     return NS_ERROR_FAILURE;
496   }
497   CGRect iconRect = ::CGRectMake(0, 0, kIconWidth, kIconHeight);
498   ::CGContextClearRect(bitmapContext, iconRect);
499   ::CGContextDrawImage(bitmapContext, iconRect, finalImage);
500   
501   CGImageRef iconImage = ::CGBitmapContextCreateImage(bitmapContext);
503   ::CGImageRelease(finalImage);
504   ::CGContextRelease(bitmapContext);
505   free(bitmap);
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;
515   }
517   [mNativeMenuItem setImage:newImage];
518   
519   [newImage release];
520   ::CGImageRelease(iconImage);
522   mLoadedIcon = PR_TRUE;
523   mSetIcon = PR_TRUE;
525   return NS_OK;
527   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
530 NS_IMETHODIMP
531 nsMenuItemIconX::OnStopContainer(imgIRequest*   aRequest,
532                                 imgIContainer* aContainer)
534   return NS_OK;
537 NS_IMETHODIMP
538 nsMenuItemIconX::OnStopDecode(imgIRequest*     aRequest,
539                              nsresult         status,
540                              const PRUnichar* statusArg)
542   return NS_OK;
545 NS_IMETHODIMP
546 nsMenuItemIconX::OnStopRequest(imgIRequest* aRequest,
547                               PRBool       aIsLastPart)
549   NS_ASSERTION(mIconRequest, "NULL mIconRequest!  Multiple calls to OnStopRequest()?");
550   if (mIconRequest) {
551     mIconRequest->Cancel(NS_BINDING_ABORTED);
552     mIconRequest = nsnull;
553   }
554   return NS_OK;
557 NS_IMETHODIMP
558 nsMenuItemIconX::OnDiscard(imgIRequest* aRequest)
560   return NS_OK;