|
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 } |