Bumping gaia.json for 2 gaia revision(s) a=gaia-bump
[gecko.git] / widget / cocoa / nsMenuItemIconX.mm
blob997d3b93038a606cf235d5327fb6cbd8699fa80d
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /*
7  * Retrieves and displays icons in native menu items on Mac OS X.
8  */
10 /* exception_defines.h defines 'try' to 'if (true)' which breaks objective-c
11    exceptions and produces errors like: error: unexpected '@' in program'.
12    If we define __EXCEPTIONS exception_defines.h will avoid doing this.
14    See bug 666609 for more information.
16    We use <limits> to get the libstdc++ version. */
17 #include <limits>
18 #if __GLIBCXX__ <= 20070719
19 #define __EXCEPTIONS
20 #endif
22 #include "nsMenuItemIconX.h"
23 #include "nsObjCExceptions.h"
24 #include "nsIContent.h"
25 #include "nsIDocument.h"
26 #include "nsNameSpaceManager.h"
27 #include "nsGkAtoms.h"
28 #include "nsIDOMElement.h"
29 #include "nsIDOMCSSStyleDeclaration.h"
30 #include "nsIDOMCSSValue.h"
31 #include "nsIDOMCSSPrimitiveValue.h"
32 #include "nsIDOMRect.h"
33 #include "nsThreadUtils.h"
34 #include "nsToolkit.h"
35 #include "nsNetUtil.h"
36 #include "imgLoader.h"
37 #include "imgRequestProxy.h"
38 #include "nsMenuItemX.h"
39 #include "gfxPlatform.h"
40 #include "imgIContainer.h"
41 #include "nsCocoaUtils.h"
42 #include "nsContentUtils.h"
44 using mozilla::gfx::SourceSurface;
45 using mozilla::RefPtr;
47 static const uint32_t kIconWidth = 16;
48 static const uint32_t kIconHeight = 16;
49 static const uint32_t kIconBitsPerComponent = 8;
50 static const uint32_t kIconComponents = 4;
51 static const uint32_t kIconBitsPerPixel = kIconBitsPerComponent *
52                                           kIconComponents;
53 static const uint32_t kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8;
54 static const uint32_t kIconBytes = kIconBytesPerRow * kIconHeight;
56 typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect,
57                              GetBottom, (nsIDOMCSSPrimitiveValue**));
59 NS_IMPL_ISUPPORTS(nsMenuItemIconX, imgINotificationObserver)
61 nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem,
62                                  nsIContent*    aContent,
63                                  NSMenuItem*    aNativeMenuItem)
64 : mContent(aContent)
65 , mMenuObject(aMenuItem)
66 , mLoadedIcon(false)
67 , mSetIcon(false)
68 , mNativeMenuItem(aNativeMenuItem)
70   //  printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem);
73 nsMenuItemIconX::~nsMenuItemIconX()
75   if (mIconRequest)
76     mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
79 // Called from mMenuObjectX's destructor, to prevent us from outliving it
80 // (as might otherwise happen if calls to our imgINotificationObserver methods
81 // are still outstanding).  mMenuObjectX owns our nNativeMenuItem.
82 void nsMenuItemIconX::Destroy()
84   if (mIconRequest) {
85     mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
86     mIconRequest = nullptr;
87   }
88   mMenuObject = nullptr;
89   mNativeMenuItem = nil;
92 nsresult
93 nsMenuItemIconX::SetupIcon()
95   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
97   // Still don't have one, then something is wrong, get out of here.
98   if (!mNativeMenuItem) {
99     NS_ERROR("No native menu item");
100     return NS_ERROR_FAILURE;
101   }
103   nsCOMPtr<nsIURI> iconURI;
104   nsresult rv = GetIconURI(getter_AddRefs(iconURI));
105   if (NS_FAILED(rv)) {
106     // There is no icon for this menu item. An icon might have been set
107     // earlier.  Clear it.
108     [mNativeMenuItem setImage:nil];
110     return NS_OK;
111   }
113   rv = LoadIcon(iconURI);
114   if (NS_FAILED(rv)) {
115     // There is no icon for this menu item, as an error occurred while loading it.
116     // An icon might have been set earlier or the place holder icon may have
117     // been set.  Clear it.
118     [mNativeMenuItem setImage:nil];
119   }
120   return rv;
122   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
125 static int32_t
126 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
128   nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
129   (aRect->*aMethod)(getter_AddRefs(dimensionValue));
130   if (!dimensionValue)
131     return -1;
133   uint16_t primitiveType;
134   nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
135   if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
136     return -1;
138   float dimension = 0;
139   rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
140                                      &dimension);
141   if (NS_FAILED(rv))
142     return -1;
144   return NSToIntRound(dimension);
147 nsresult
148 nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
150   if (!mMenuObject)
151     return NS_ERROR_FAILURE;
153   // Mac native menu items support having both a checkmark and an icon
154   // simultaneously, but this is unheard of in the cross-platform toolkit,
155   // seemingly because the win32 theme is unable to cope with both at once.
156   // The downside is that it's possible to get a menu item marked with a
157   // native checkmark and a checkmark for an icon.  Head off that possibility
158   // by pretending that no icon exists if this is a checkable menu item.
159   if (mMenuObject->MenuObjectType() == eMenuItemObjectType) {
160     nsMenuItemX* menuItem = static_cast<nsMenuItemX*>(mMenuObject);
161     if (menuItem->GetMenuItemType() != eRegularMenuItemType)
162       return NS_ERROR_FAILURE;
163   }
165   if (!mContent)
166     return NS_ERROR_FAILURE;
168   // First, look at the content node's "image" attribute.
169   nsAutoString imageURIString;
170   bool hasImageAttr = mContent->GetAttr(kNameSpaceID_None,
171                                         nsGkAtoms::image,
172                                         imageURIString);
174   nsresult rv;
175   nsCOMPtr<nsIDOMCSSValue> cssValue;
176   nsCOMPtr<nsIDOMCSSStyleDeclaration> cssStyleDecl;
177   nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
178   uint16_t primitiveType;
179   if (!hasImageAttr) {
180     // If the content node has no "image" attribute, get the
181     // "list-style-image" property from CSS.
182     nsCOMPtr<nsIDocument> document = mContent->GetComposedDoc();
183     if (!document)
184       return NS_ERROR_FAILURE;
186     nsCOMPtr<nsPIDOMWindow> window = document->GetWindow();
187     if (!window)
188       return NS_ERROR_FAILURE;
190     nsCOMPtr<nsIDOMElement> domElement = do_QueryInterface(mContent);
191     if (!domElement)
192       return NS_ERROR_FAILURE;
195     rv = window->GetComputedStyle(domElement, EmptyString(),
196                                   getter_AddRefs(cssStyleDecl));
197     if (NS_FAILED(rv))
198       return rv;
200     NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image");
201     rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage,
202                                            getter_AddRefs(cssValue));
203     if (NS_FAILED(rv)) return rv;
205     primitiveValue = do_QueryInterface(cssValue);
206     if (!primitiveValue) return NS_ERROR_FAILURE;
208     rv = primitiveValue->GetPrimitiveType(&primitiveType);
209     if (NS_FAILED(rv)) return rv;
210     if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI)
211       return NS_ERROR_FAILURE;
213     rv = primitiveValue->GetStringValue(imageURIString);
214     if (NS_FAILED(rv)) return rv;
215   }
217   // Empty the mImageRegionRect initially as the image region CSS could
218   // have been changed and now have an error or have been removed since the
219   // last GetIconURI call.
220   mImageRegionRect.SetEmpty();
222   // If this menu item shouldn't have an icon, the string will be empty,
223   // and NS_NewURI will fail.
224   nsCOMPtr<nsIURI> iconURI;
225   rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
226   if (NS_FAILED(rv)) return rv;
228   *aIconURI = iconURI;
229   NS_ADDREF(*aIconURI);
231   if (!hasImageAttr) {
232     // Check if the icon has a specified image region so that it can be
233     // cropped appropriately before being displayed.
234     NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region");
235     rv = cssStyleDecl->GetPropertyCSSValue(imageRegion,
236                                            getter_AddRefs(cssValue));
237     // Just return NS_OK if there if there is a failure due to no
238     // moz-image region specified so the whole icon will be drawn anyway.
239     if (NS_FAILED(rv)) return NS_OK;
241     primitiveValue = do_QueryInterface(cssValue);
242     if (!primitiveValue) return NS_OK;
244     rv = primitiveValue->GetPrimitiveType(&primitiveType);
245     if (NS_FAILED(rv)) return NS_OK;
246     if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT)
247       return NS_OK;
249     nsCOMPtr<nsIDOMRect> imageRegionRect;
250     rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect));
251     if (NS_FAILED(rv)) return NS_OK;
253     if (imageRegionRect) {
254       // Return NS_ERROR_FAILURE if the image region is invalid so the image
255       // is not drawn, and behavior is similar to XUL menus.
256       int32_t bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom);
257       int32_t right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight);
258       int32_t top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop);
259       int32_t left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft);
261       if (top < 0 || left < 0 || bottom <= top || right <= left)
262         return NS_ERROR_FAILURE;
264       mImageRegionRect.SetRect(left, top, right - left, bottom - top);
265     }
266   }
268   return NS_OK;
271 nsresult
272 nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
274   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
276   if (mIconRequest) {
277     // Another icon request is already in flight.  Kill it.
278     mIconRequest->Cancel(NS_BINDING_ABORTED);
279     mIconRequest = nullptr;
280   }
282   mLoadedIcon = false;
284   if (!mContent) return NS_ERROR_FAILURE;
286   nsCOMPtr<nsIDocument> document = mContent->OwnerDoc();
288   nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup();
289   if (!loadGroup) return NS_ERROR_FAILURE;
291   nsRefPtr<imgLoader> loader = nsContentUtils::GetImgLoaderForDocument(document);
292   if (!loader) return NS_ERROR_FAILURE;
294   if (!mSetIcon) {
295     // Set a completely transparent 16x16 image as the icon on this menu item
296     // as a placeholder.  This keeps the menu item text displayed in the same
297     // position that it will be displayed when the real icon is loaded, and
298     // prevents it from jumping around or looking misaligned.
300     static bool sInitializedPlaceholder;
301     static NSImage* sPlaceholderIconImage;
302     if (!sInitializedPlaceholder) {
303       sInitializedPlaceholder = true;
305       // Note that we only create the one and reuse it forever, so this is not a leak.
306       sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)];
307     }
309     if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
311     if (mNativeMenuItem)
312       [mNativeMenuItem setImage:sPlaceholderIconImage];
313   }
315   // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is
316   // not exposed to web content
317   nsresult rv = loader->LoadImage(aIconURI, nullptr, nullptr, nullptr, loadGroup, this,
318                                    nullptr, nsIRequest::LOAD_NORMAL, nullptr,
319                                    nullptr, EmptyString(), getter_AddRefs(mIconRequest));
320   if (NS_FAILED(rv)) return rv;
322   // We need to request the icon be decoded (bug 573583, bug 705516).
323   mIconRequest->StartDecoding();
325   return NS_OK;
327   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
331 // imgINotificationObserver
334 NS_IMETHODIMP
335 nsMenuItemIconX::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData)
337   if (aType == imgINotificationObserver::FRAME_COMPLETE) {
338     return OnStopFrame(aRequest);
339   }
341   if (aType == imgINotificationObserver::DECODE_COMPLETE) {
342     if (mIconRequest && mIconRequest == aRequest) {
343       mIconRequest->Cancel(NS_BINDING_ABORTED);
344       mIconRequest = nullptr;
345     }
346   }
348   return NS_OK;
351 nsresult
352 nsMenuItemIconX::OnStopFrame(imgIRequest*    aRequest)
354   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
356   if (aRequest != mIconRequest)
357     return NS_ERROR_FAILURE;
359   // Only support one frame.
360   if (mLoadedIcon)
361     return NS_OK;
363   if (!mNativeMenuItem)
364     return NS_ERROR_FAILURE;
366   nsCOMPtr<imgIContainer> imageContainer;
367   aRequest->GetImage(getter_AddRefs(imageContainer));
368   if (!imageContainer) {
369     [mNativeMenuItem setImage:nil];
370     return NS_ERROR_FAILURE;
371   }
373   int32_t origWidth = 0, origHeight = 0;
374   imageContainer->GetWidth(&origWidth);
375   imageContainer->GetHeight(&origHeight);
376   
377   // If the image region is invalid, don't draw the image to almost match
378   // the behavior of other platforms.
379   if (!mImageRegionRect.IsEmpty() &&
380       (mImageRegionRect.XMost() > origWidth ||
381        mImageRegionRect.YMost() > origHeight)) {
382     [mNativeMenuItem setImage:nil];
383     return NS_ERROR_FAILURE;
384   }
386   if (mImageRegionRect.IsEmpty()) {
387     mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
388   }
390   RefPtr<SourceSurface> surface =
391     imageContainer->GetFrame(imgIContainer::FRAME_CURRENT,
392                              imgIContainer::FLAG_SYNC_DECODE);
393   if (!surface) {
394     [mNativeMenuItem setImage:nil];
395     return NS_ERROR_FAILURE;
396   }
398   CGImageRef origImage = NULL;
399   nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &origImage);
400   if (NS_FAILED(rv) || !origImage) {
401     [mNativeMenuItem setImage:nil];
402     return NS_ERROR_FAILURE;
403   }
405   bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
406                             mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
407   
408   CGImageRef finalImage = origImage;
409   if (createSubImage) {
410     // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall 
411     // image to use as the icon
412     finalImage = ::CGImageCreateWithImageInRect(origImage, 
413                                                 ::CGRectMake(mImageRegionRect.x, 
414                                                 mImageRegionRect.y,
415                                                 mImageRegionRect.width,
416                                                 mImageRegionRect.height));
417     ::CGImageRelease(origImage);
418     if (!finalImage) {
419       [mNativeMenuItem setImage:nil];
420       return NS_ERROR_FAILURE;  
421     }
422   }
424   NSImage *newImage = nil;
425   rv = nsCocoaUtils::CreateNSImageFromCGImage(finalImage, &newImage);
426   if (NS_FAILED(rv) || !newImage) {    
427     [mNativeMenuItem setImage:nil];
428     ::CGImageRelease(finalImage);
429     return NS_ERROR_FAILURE;
430   }
432   [newImage setSize:NSMakeSize(kIconWidth, kIconHeight)];
433   [mNativeMenuItem setImage:newImage];
434   
435   [newImage release];
436   ::CGImageRelease(finalImage);
438   mLoadedIcon = true;
439   mSetIcon = true;
441   if (mMenuObject) {
442     mMenuObject->IconUpdated();
443   }
445   return NS_OK;
447   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;