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/. */
7 * Retrieves and displays icons in native menu items on Mac OS X.
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. */
18 #if __GLIBCXX__ <= 20070719
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 *
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,
63 NSMenuItem* aNativeMenuItem)
65 , mMenuObject(aMenuItem)
68 , mNativeMenuItem(aNativeMenuItem)
70 // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem);
73 nsMenuItemIconX::~nsMenuItemIconX()
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()
85 mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
86 mIconRequest = nullptr;
88 mMenuObject = nullptr;
89 mNativeMenuItem = nil;
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;
103 nsCOMPtr<nsIURI> iconURI;
104 nsresult rv = GetIconURI(getter_AddRefs(iconURI));
106 // There is no icon for this menu item. An icon might have been set
107 // earlier. Clear it.
108 [mNativeMenuItem setImage:nil];
113 rv = LoadIcon(iconURI);
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];
122 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
126 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
128 nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
129 (aRect->*aMethod)(getter_AddRefs(dimensionValue));
133 uint16_t primitiveType;
134 nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
135 if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
139 rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
144 return NSToIntRound(dimension);
148 nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
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;
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,
175 nsCOMPtr<nsIDOMCSSValue> cssValue;
176 nsCOMPtr<nsIDOMCSSStyleDeclaration> cssStyleDecl;
177 nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
178 uint16_t primitiveType;
180 // If the content node has no "image" attribute, get the
181 // "list-style-image" property from CSS.
182 nsCOMPtr<nsIDocument> document = mContent->GetComposedDoc();
184 return NS_ERROR_FAILURE;
186 nsCOMPtr<nsPIDOMWindow> window = document->GetWindow();
188 return NS_ERROR_FAILURE;
190 nsCOMPtr<nsIDOMElement> domElement = do_QueryInterface(mContent);
192 return NS_ERROR_FAILURE;
195 rv = window->GetComputedStyle(domElement, EmptyString(),
196 getter_AddRefs(cssStyleDecl));
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;
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;
229 NS_ADDREF(*aIconURI);
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)
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);
272 nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
274 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
277 // Another icon request is already in flight. Kill it.
278 mIconRequest->Cancel(NS_BINDING_ABORTED);
279 mIconRequest = nullptr;
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;
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)];
309 if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
312 [mNativeMenuItem setImage:sPlaceholderIconImage];
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();
327 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
331 // imgINotificationObserver
335 nsMenuItemIconX::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData)
337 if (aType == imgINotificationObserver::FRAME_COMPLETE) {
338 return OnStopFrame(aRequest);
341 if (aType == imgINotificationObserver::DECODE_COMPLETE) {
342 if (mIconRequest && mIconRequest == aRequest) {
343 mIconRequest->Cancel(NS_BINDING_ABORTED);
344 mIconRequest = nullptr;
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.
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;
373 int32_t origWidth = 0, origHeight = 0;
374 imageContainer->GetWidth(&origWidth);
375 imageContainer->GetHeight(&origHeight);
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;
386 if (mImageRegionRect.IsEmpty()) {
387 mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
390 RefPtr<SourceSurface> surface =
391 imageContainer->GetFrame(imgIContainer::FRAME_CURRENT,
392 imgIContainer::FLAG_SYNC_DECODE);
394 [mNativeMenuItem setImage:nil];
395 return NS_ERROR_FAILURE;
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;
405 bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
406 mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
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,
415 mImageRegionRect.width,
416 mImageRegionRect.height));
417 ::CGImageRelease(origImage);
419 [mNativeMenuItem setImage:nil];
420 return NS_ERROR_FAILURE;
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;
432 [newImage setSize:NSMakeSize(kIconWidth, kIconHeight)];
433 [mNativeMenuItem setImage:newImage];
436 ::CGImageRelease(finalImage);
442 mMenuObject->IconUpdated();
447 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;