widget/cocoa/nsMenuItemIconX.mm

changeset 2
7e26c7da4463
equal deleted inserted replaced
-1:000000000000 0:500d5e8f4143
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/. */
5
6 /*
7 * Retrieves and displays icons in native menu items on Mac OS X.
8 */
9
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.
13
14 See bug 666609 for more information.
15
16 We use <limits> to get the libstdc++ version. */
17 #include <limits>
18 #if __GLIBCXX__ <= 20070719
19 #define __EXCEPTIONS
20 #endif
21
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 "mozIThirdPartyUtil.h"
43 #include "nsContentUtils.h"
44
45 using mozilla::gfx::SourceSurface;
46 using mozilla::RefPtr;
47
48 static const uint32_t kIconWidth = 16;
49 static const uint32_t kIconHeight = 16;
50 static const uint32_t kIconBitsPerComponent = 8;
51 static const uint32_t kIconComponents = 4;
52 static const uint32_t kIconBitsPerPixel = kIconBitsPerComponent *
53 kIconComponents;
54 static const uint32_t kIconBytesPerRow = kIconWidth * kIconBitsPerPixel / 8;
55 static const uint32_t kIconBytes = kIconBytesPerRow * kIconHeight;
56
57 typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect,
58 GetBottom, (nsIDOMCSSPrimitiveValue**));
59
60 NS_IMPL_ISUPPORTS(nsMenuItemIconX, imgINotificationObserver)
61
62 nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem,
63 nsIContent* aContent,
64 NSMenuItem* aNativeMenuItem)
65 : mContent(aContent)
66 , mMenuObject(aMenuItem)
67 , mLoadedIcon(false)
68 , mSetIcon(false)
69 , mNativeMenuItem(aNativeMenuItem)
70 {
71 // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem);
72 }
73
74 nsMenuItemIconX::~nsMenuItemIconX()
75 {
76 if (mIconRequest)
77 mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
78 }
79
80 // Called from mMenuObjectX's destructor, to prevent us from outliving it
81 // (as might otherwise happen if calls to our imgINotificationObserver methods
82 // are still outstanding). mMenuObjectX owns our nNativeMenuItem.
83 void nsMenuItemIconX::Destroy()
84 {
85 if (mIconRequest) {
86 mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
87 mIconRequest = nullptr;
88 }
89 mMenuObject = nullptr;
90 mNativeMenuItem = nil;
91 }
92
93 nsresult
94 nsMenuItemIconX::SetupIcon()
95 {
96 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
97
98 // Still don't have one, then something is wrong, get out of here.
99 if (!mNativeMenuItem) {
100 NS_ERROR("No native menu item");
101 return NS_ERROR_FAILURE;
102 }
103
104 nsCOMPtr<nsIURI> iconURI;
105 nsresult rv = GetIconURI(getter_AddRefs(iconURI));
106 if (NS_FAILED(rv)) {
107 // There is no icon for this menu item. An icon might have been set
108 // earlier. Clear it.
109 [mNativeMenuItem setImage:nil];
110
111 return NS_OK;
112 }
113
114 rv = LoadIcon(iconURI);
115 if (NS_FAILED(rv)) {
116 // There is no icon for this menu item, as an error occurred while loading it.
117 // An icon might have been set earlier or the place holder icon may have
118 // been set. Clear it.
119 [mNativeMenuItem setImage:nil];
120 }
121 return rv;
122
123 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
124 }
125
126 static int32_t
127 GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod)
128 {
129 nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue;
130 (aRect->*aMethod)(getter_AddRefs(dimensionValue));
131 if (!dimensionValue)
132 return -1;
133
134 uint16_t primitiveType;
135 nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType);
136 if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX)
137 return -1;
138
139 float dimension = 0;
140 rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX,
141 &dimension);
142 if (NS_FAILED(rv))
143 return -1;
144
145 return NSToIntRound(dimension);
146 }
147
148 nsresult
149 nsMenuItemIconX::GetIconURI(nsIURI** aIconURI)
150 {
151 if (!mMenuObject)
152 return NS_ERROR_FAILURE;
153
154 // Mac native menu items support having both a checkmark and an icon
155 // simultaneously, but this is unheard of in the cross-platform toolkit,
156 // seemingly because the win32 theme is unable to cope with both at once.
157 // The downside is that it's possible to get a menu item marked with a
158 // native checkmark and a checkmark for an icon. Head off that possibility
159 // by pretending that no icon exists if this is a checkable menu item.
160 if (mMenuObject->MenuObjectType() == eMenuItemObjectType) {
161 nsMenuItemX* menuItem = static_cast<nsMenuItemX*>(mMenuObject);
162 if (menuItem->GetMenuItemType() != eRegularMenuItemType)
163 return NS_ERROR_FAILURE;
164 }
165
166 if (!mContent)
167 return NS_ERROR_FAILURE;
168
169 // First, look at the content node's "image" attribute.
170 nsAutoString imageURIString;
171 bool hasImageAttr = mContent->GetAttr(kNameSpaceID_None,
172 nsGkAtoms::image,
173 imageURIString);
174
175 nsresult rv;
176 nsCOMPtr<nsIDOMCSSValue> cssValue;
177 nsCOMPtr<nsIDOMCSSStyleDeclaration> cssStyleDecl;
178 nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue;
179 uint16_t primitiveType;
180 if (!hasImageAttr) {
181 // If the content node has no "image" attribute, get the
182 // "list-style-image" property from CSS.
183 nsCOMPtr<nsIDocument> document = mContent->GetDocument();
184 if (!document)
185 return NS_ERROR_FAILURE;
186
187 nsCOMPtr<nsPIDOMWindow> window = document->GetWindow();
188 if (!window)
189 return NS_ERROR_FAILURE;
190
191 nsCOMPtr<nsIDOMElement> domElement = do_QueryInterface(mContent);
192 if (!domElement)
193 return NS_ERROR_FAILURE;
194
195
196 rv = window->GetComputedStyle(domElement, EmptyString(),
197 getter_AddRefs(cssStyleDecl));
198 if (NS_FAILED(rv))
199 return rv;
200
201 NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image");
202 rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage,
203 getter_AddRefs(cssValue));
204 if (NS_FAILED(rv)) return rv;
205
206 primitiveValue = do_QueryInterface(cssValue);
207 if (!primitiveValue) return NS_ERROR_FAILURE;
208
209 rv = primitiveValue->GetPrimitiveType(&primitiveType);
210 if (NS_FAILED(rv)) return rv;
211 if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI)
212 return NS_ERROR_FAILURE;
213
214 rv = primitiveValue->GetStringValue(imageURIString);
215 if (NS_FAILED(rv)) return rv;
216 }
217
218 // Empty the mImageRegionRect initially as the image region CSS could
219 // have been changed and now have an error or have been removed since the
220 // last GetIconURI call.
221 mImageRegionRect.SetEmpty();
222
223 // If this menu item shouldn't have an icon, the string will be empty,
224 // and NS_NewURI will fail.
225 nsCOMPtr<nsIURI> iconURI;
226 rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
227 if (NS_FAILED(rv)) return rv;
228
229 *aIconURI = iconURI;
230 NS_ADDREF(*aIconURI);
231
232 if (!hasImageAttr) {
233 // Check if the icon has a specified image region so that it can be
234 // cropped appropriately before being displayed.
235 NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region");
236 rv = cssStyleDecl->GetPropertyCSSValue(imageRegion,
237 getter_AddRefs(cssValue));
238 // Just return NS_OK if there if there is a failure due to no
239 // moz-image region specified so the whole icon will be drawn anyway.
240 if (NS_FAILED(rv)) return NS_OK;
241
242 primitiveValue = do_QueryInterface(cssValue);
243 if (!primitiveValue) return NS_OK;
244
245 rv = primitiveValue->GetPrimitiveType(&primitiveType);
246 if (NS_FAILED(rv)) return NS_OK;
247 if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT)
248 return NS_OK;
249
250 nsCOMPtr<nsIDOMRect> imageRegionRect;
251 rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect));
252 if (NS_FAILED(rv)) return NS_OK;
253
254 if (imageRegionRect) {
255 // Return NS_ERROR_FAILURE if the image region is invalid so the image
256 // is not drawn, and behavior is similar to XUL menus.
257 int32_t bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom);
258 int32_t right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight);
259 int32_t top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop);
260 int32_t left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft);
261
262 if (top < 0 || left < 0 || bottom <= top || right <= left)
263 return NS_ERROR_FAILURE;
264
265 mImageRegionRect.SetRect(left, top, right - left, bottom - top);
266 }
267 }
268
269 return NS_OK;
270 }
271
272 nsresult
273 nsMenuItemIconX::LoadIcon(nsIURI* aIconURI)
274 {
275 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
276
277 if (mIconRequest) {
278 // Another icon request is already in flight. Kill it.
279 mIconRequest->Cancel(NS_BINDING_ABORTED);
280 mIconRequest = nullptr;
281 }
282
283 mLoadedIcon = false;
284
285 if (!mContent) return NS_ERROR_FAILURE;
286
287 nsCOMPtr<nsIDocument> document = mContent->OwnerDoc();
288
289 nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup();
290 if (!loadGroup) return NS_ERROR_FAILURE;
291
292 nsRefPtr<imgLoader> loader = nsContentUtils::GetImgLoaderForDocument(document);
293 if (!loader) return NS_ERROR_FAILURE;
294
295 if (!mSetIcon) {
296 // Set a completely transparent 16x16 image as the icon on this menu item
297 // as a placeholder. This keeps the menu item text displayed in the same
298 // position that it will be displayed when the real icon is loaded, and
299 // prevents it from jumping around or looking misaligned.
300
301 static bool sInitializedPlaceholder;
302 static NSImage* sPlaceholderIconImage;
303 if (!sInitializedPlaceholder) {
304 sInitializedPlaceholder = true;
305
306 // Note that we only create the one and reuse it forever, so this is not a leak.
307 sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)];
308 }
309
310 if (!sPlaceholderIconImage) return NS_ERROR_FAILURE;
311
312 if (mNativeMenuItem)
313 [mNativeMenuItem setImage:sPlaceholderIconImage];
314 }
315
316 nsCOMPtr<nsIURI> firstPartyIsolationURI;
317 nsCOMPtr<mozIThirdPartyUtil> thirdPartySvc
318 = do_GetService(THIRDPARTYUTIL_CONTRACTID);
319 thirdPartySvc->GetFirstPartyURI(nullptr, document,
320 getter_AddRefs(firstPartyIsolationURI));
321
322 // Passing in null for channelPolicy here since nsMenuItemIconX::LoadIcon is
323 // not exposed to web content
324 nsresult rv = loader->LoadImage(aIconURI, firstPartyIsolationURI, nullptr, nullptr, loadGroup, this,
325 nullptr, nsIRequest::LOAD_NORMAL, nullptr,
326 nullptr, EmptyString(), getter_AddRefs(mIconRequest));
327 if (NS_FAILED(rv)) return rv;
328
329 // We need to request the icon be decoded (bug 573583, bug 705516).
330 mIconRequest->StartDecoding();
331
332 return NS_OK;
333
334 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
335 }
336
337 //
338 // imgINotificationObserver
339 //
340
341 NS_IMETHODIMP
342 nsMenuItemIconX::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData)
343 {
344 if (aType == imgINotificationObserver::FRAME_COMPLETE) {
345 return OnStopFrame(aRequest);
346 }
347
348 if (aType == imgINotificationObserver::DECODE_COMPLETE) {
349 if (mIconRequest && mIconRequest == aRequest) {
350 mIconRequest->Cancel(NS_BINDING_ABORTED);
351 mIconRequest = nullptr;
352 }
353 }
354
355 return NS_OK;
356 }
357
358 nsresult
359 nsMenuItemIconX::OnStopFrame(imgIRequest* aRequest)
360 {
361 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
362
363 if (aRequest != mIconRequest)
364 return NS_ERROR_FAILURE;
365
366 // Only support one frame.
367 if (mLoadedIcon)
368 return NS_OK;
369
370 if (!mNativeMenuItem)
371 return NS_ERROR_FAILURE;
372
373 nsCOMPtr<imgIContainer> imageContainer;
374 aRequest->GetImage(getter_AddRefs(imageContainer));
375 if (!imageContainer) {
376 [mNativeMenuItem setImage:nil];
377 return NS_ERROR_FAILURE;
378 }
379
380 int32_t origWidth = 0, origHeight = 0;
381 imageContainer->GetWidth(&origWidth);
382 imageContainer->GetHeight(&origHeight);
383
384 // If the image region is invalid, don't draw the image to almost match
385 // the behavior of other platforms.
386 if (!mImageRegionRect.IsEmpty() &&
387 (mImageRegionRect.XMost() > origWidth ||
388 mImageRegionRect.YMost() > origHeight)) {
389 [mNativeMenuItem setImage:nil];
390 return NS_ERROR_FAILURE;
391 }
392
393 if (mImageRegionRect.IsEmpty()) {
394 mImageRegionRect.SetRect(0, 0, origWidth, origHeight);
395 }
396
397 RefPtr<SourceSurface> surface =
398 imageContainer->GetFrame(imgIContainer::FRAME_CURRENT,
399 imgIContainer::FLAG_NONE);
400 if (!surface) {
401 [mNativeMenuItem setImage:nil];
402 return NS_ERROR_FAILURE;
403 }
404
405 CGImageRef origImage = NULL;
406 nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &origImage);
407 if (NS_FAILED(rv) || !origImage) {
408 [mNativeMenuItem setImage:nil];
409 return NS_ERROR_FAILURE;
410 }
411
412 bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 &&
413 mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight);
414
415 CGImageRef finalImage = NULL;
416 if (createSubImage) {
417 // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall
418 // image to use as the icon
419 finalImage = ::CGImageCreateWithImageInRect(origImage,
420 ::CGRectMake(mImageRegionRect.x,
421 mImageRegionRect.y,
422 mImageRegionRect.width,
423 mImageRegionRect.height));
424 ::CGImageRelease(origImage);
425 if (!finalImage) {
426 [mNativeMenuItem setImage:nil];
427 return NS_ERROR_FAILURE;
428 }
429 } else {
430 finalImage = origImage;
431 }
432 // The image may not be the right size for a menu icon (16x16).
433 // Create a new CGImage for the menu item.
434 uint8_t* bitmap = (uint8_t*)malloc(kIconBytes);
435
436 CGColorSpaceRef colorSpace = ::CGColorSpaceCreateDeviceRGB();
437
438 CGContextRef bitmapContext = ::CGBitmapContextCreate(bitmap, kIconWidth, kIconHeight,
439 kIconBitsPerComponent,
440 kIconBytesPerRow,
441 colorSpace,
442 kCGImageAlphaPremultipliedLast);
443 ::CGColorSpaceRelease(colorSpace);
444 if (!bitmapContext) {
445 ::CGImageRelease(finalImage);
446 free(bitmap);
447 ::CGColorSpaceRelease(colorSpace);
448 return NS_ERROR_FAILURE;
449 }
450 CGRect iconRect = ::CGRectMake(0, 0, kIconWidth, kIconHeight);
451 ::CGContextClearRect(bitmapContext, iconRect);
452 ::CGContextDrawImage(bitmapContext, iconRect, finalImage);
453
454 CGImageRef iconImage = ::CGBitmapContextCreateImage(bitmapContext);
455
456 ::CGImageRelease(finalImage);
457 ::CGContextRelease(bitmapContext);
458 free(bitmap);
459
460 if (!iconImage) return NS_ERROR_FAILURE;
461
462 NSImage *newImage = nil;
463 rv = nsCocoaUtils::CreateNSImageFromCGImage(iconImage, &newImage);
464 if (NS_FAILED(rv) || !newImage) {
465 [mNativeMenuItem setImage:nil];
466 ::CGImageRelease(iconImage);
467 return NS_ERROR_FAILURE;
468 }
469
470 [mNativeMenuItem setImage:newImage];
471
472 [newImage release];
473 ::CGImageRelease(iconImage);
474
475 mLoadedIcon = true;
476 mSetIcon = true;
477
478 return NS_OK;
479
480 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
481 }

mercurial