Bug 1793629 - Implement attention indicator for the unified extensions button, r...
[gecko.git] / widget / cocoa / nsCocoaUtils.mm
blob7eee462df11e3a3f5094d6d835fd7f56c6c611b7
1 /* -*- Mode: C++; tab-width: 20; 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 #import <AVFoundation/AVFoundation.h>
8 #include <cmath>
10 #include "AppleUtils.h"
11 #include "gfx2DGlue.h"
12 #include "gfxContext.h"
13 #include "gfxPlatform.h"
14 #include "gfxUtils.h"
15 #include "ImageRegion.h"
16 #include "nsCocoaUtils.h"
17 #include "nsChildView.h"
18 #include "nsMenuBarX.h"
19 #include "nsCocoaFeatures.h"
20 #include "nsCocoaWindow.h"
21 #include "nsCOMPtr.h"
22 #include "nsIInterfaceRequestorUtils.h"
23 #include "nsIAppShellService.h"
24 #include "nsIOSPermissionRequest.h"
25 #include "nsIRunnable.h"
26 #include "nsIAppWindow.h"
27 #include "nsIBaseWindow.h"
28 #include "nsMenuUtilsX.h"
29 #include "nsNetUtil.h"
30 #include "nsToolkit.h"
31 #include "nsCRT.h"
32 #include "mozilla/ClearOnShutdown.h"
33 #include "mozilla/Logging.h"
34 #include "mozilla/MiscEvents.h"
35 #include "mozilla/Preferences.h"
36 #include "mozilla/TextEvents.h"
37 #include "mozilla/StaticMutex.h"
38 #include "mozilla/StaticPrefs_media.h"
39 #include "mozilla/SVGImageContext.h"
40 #include "mozilla/dom/Promise.h"
41 #include "mozilla/gfx/2D.h"
43 using namespace mozilla;
44 using namespace mozilla::widget;
46 using mozilla::dom::Promise;
47 using mozilla::gfx::DataSourceSurface;
48 using mozilla::gfx::DrawTarget;
49 using mozilla::gfx::SamplingFilter;
50 using mozilla::gfx::IntPoint;
51 using mozilla::gfx::IntRect;
52 using mozilla::gfx::IntSize;
53 using mozilla::gfx::SurfaceFormat;
54 using mozilla::gfx::SourceSurface;
55 using mozilla::image::ImageRegion;
57 LazyLogModule gCocoaUtilsLog("nsCocoaUtils");
58 #undef LOG
59 #define LOG(...) MOZ_LOG(gCocoaUtilsLog, LogLevel::Debug, (__VA_ARGS__))
62  * For each audio and video capture request, we hold an owning reference
63  * to a promise to be resolved when the request's async callback is invoked.
64  * sVideoCapturePromises and sAudioCapturePromises are arrays of video and
65  * audio promises waiting for to be resolved. Each array is protected by a
66  * mutex.
67  */
68 nsCocoaUtils::PromiseArray nsCocoaUtils::sVideoCapturePromises;
69 nsCocoaUtils::PromiseArray nsCocoaUtils::sAudioCapturePromises;
70 StaticMutex nsCocoaUtils::sMediaCaptureMutex;
72 static float MenuBarScreenHeight() {
73   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
75   NSArray* allScreens = [NSScreen screens];
76   if ([allScreens count]) {
77     return [[allScreens objectAtIndex:0] frame].size.height;
78   }
80   return 0.0;
82   NS_OBJC_END_TRY_BLOCK_RETURN(0.0);
85 float nsCocoaUtils::FlippedScreenY(float y) { return MenuBarScreenHeight() - y; }
87 NSRect nsCocoaUtils::GeckoRectToCocoaRect(const DesktopIntRect& geckoRect) {
88   // We only need to change the Y coordinate by starting with the primary screen
89   // height and subtracting the gecko Y coordinate of the bottom of the rect.
90   return NSMakeRect(geckoRect.x, MenuBarScreenHeight() - geckoRect.YMost(), geckoRect.width,
91                     geckoRect.height);
94 NSPoint nsCocoaUtils::GeckoPointToCocoaPoint(const mozilla::DesktopPoint& aPoint) {
95   return NSMakePoint(aPoint.x, MenuBarScreenHeight() - aPoint.y);
98 NSRect nsCocoaUtils::GeckoRectToCocoaRectDevPix(const LayoutDeviceIntRect& aGeckoRect,
99                                                 CGFloat aBackingScale) {
100   return NSMakeRect(aGeckoRect.x / aBackingScale,
101                     MenuBarScreenHeight() - aGeckoRect.YMost() / aBackingScale,
102                     aGeckoRect.width / aBackingScale, aGeckoRect.height / aBackingScale);
105 DesktopIntRect nsCocoaUtils::CocoaRectToGeckoRect(const NSRect& cocoaRect) {
106   // We only need to change the Y coordinate by starting with the primary screen
107   // height and subtracting both the cocoa y origin and the height of the
108   // cocoa rect.
109   DesktopIntRect rect;
110   rect.x = NSToIntRound(cocoaRect.origin.x);
111   rect.y = NSToIntRound(FlippedScreenY(cocoaRect.origin.y + cocoaRect.size.height));
112   rect.width = NSToIntRound(cocoaRect.origin.x + cocoaRect.size.width) - rect.x;
113   rect.height = NSToIntRound(FlippedScreenY(cocoaRect.origin.y)) - rect.y;
114   return rect;
117 LayoutDeviceIntRect nsCocoaUtils::CocoaRectToGeckoRectDevPix(const NSRect& aCocoaRect,
118                                                              CGFloat aBackingScale) {
119   LayoutDeviceIntRect rect;
120   rect.x = NSToIntRound(aCocoaRect.origin.x * aBackingScale);
121   rect.y =
122       NSToIntRound(FlippedScreenY(aCocoaRect.origin.y + aCocoaRect.size.height) * aBackingScale);
123   rect.width = NSToIntRound((aCocoaRect.origin.x + aCocoaRect.size.width) * aBackingScale) - rect.x;
124   rect.height = NSToIntRound(FlippedScreenY(aCocoaRect.origin.y) * aBackingScale) - rect.y;
125   return rect;
128 NSPoint nsCocoaUtils::ScreenLocationForEvent(NSEvent* anEvent) {
129   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
131   // Don't trust mouse locations of mouse move events, see bug 443178.
132   if (!anEvent || [anEvent type] == NSEventTypeMouseMoved) return [NSEvent mouseLocation];
134   // Pin momentum scroll events to the location of the last user-controlled
135   // scroll event.
136   if (IsMomentumScrollEvent(anEvent)) return ChildViewMouseTracker::sLastScrollEventScreenLocation;
138   return nsCocoaUtils::ConvertPointToScreen([anEvent window], [anEvent locationInWindow]);
140   NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
143 BOOL nsCocoaUtils::IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow) {
144   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
146   return NSPointInRect(ScreenLocationForEvent(anEvent), [aWindow frame]);
148   NS_OBJC_END_TRY_BLOCK_RETURN(NO);
151 NSPoint nsCocoaUtils::EventLocationForWindow(NSEvent* anEvent, NSWindow* aWindow) {
152   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
154   return nsCocoaUtils::ConvertPointFromScreen(aWindow, ScreenLocationForEvent(anEvent));
156   NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
159 BOOL nsCocoaUtils::IsMomentumScrollEvent(NSEvent* aEvent) {
160   return [aEvent type] == NSEventTypeScrollWheel && [aEvent momentumPhase] != NSEventPhaseNone;
163 BOOL nsCocoaUtils::EventHasPhaseInformation(NSEvent* aEvent) {
164   return [aEvent phase] != NSEventPhaseNone || [aEvent momentumPhase] != NSEventPhaseNone;
167 void nsCocoaUtils::HideOSChromeOnScreen(bool aShouldHide) {
168   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
170   // Keep track of how many hiding requests have been made, so that they can
171   // be nested.
172   static int sHiddenCount = 0;
174   sHiddenCount += aShouldHide ? 1 : -1;
175   NS_ASSERTION(sHiddenCount >= 0, "Unbalanced HideMenuAndDockForWindow calls");
177   NSApplicationPresentationOptions options =
178       sHiddenCount <= 0 ? NSApplicationPresentationDefault
179                         : NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
180   [NSApp setPresentationOptions:options];
182   NS_OBJC_END_TRY_IGNORE_BLOCK;
185 #define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
186 nsIWidget* nsCocoaUtils::GetHiddenWindowWidget() {
187   nsCOMPtr<nsIAppShellService> appShell(do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
188   if (!appShell) {
189     NS_WARNING("Couldn't get AppShellService in order to get hidden window ref");
190     return nullptr;
191   }
193   nsCOMPtr<nsIAppWindow> hiddenWindow;
194   appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow));
195   if (!hiddenWindow) {
196     // Don't warn, this happens during shutdown, bug 358607.
197     return nullptr;
198   }
200   nsCOMPtr<nsIBaseWindow> baseHiddenWindow;
201   baseHiddenWindow = do_GetInterface(hiddenWindow);
202   if (!baseHiddenWindow) {
203     NS_WARNING("Couldn't get nsIBaseWindow from hidden window (nsIAppWindow)");
204     return nullptr;
205   }
207   nsCOMPtr<nsIWidget> hiddenWindowWidget;
208   if (NS_FAILED(baseHiddenWindow->GetMainWidget(getter_AddRefs(hiddenWindowWidget)))) {
209     NS_WARNING("Couldn't get nsIWidget from hidden window (nsIBaseWindow)");
210     return nullptr;
211   }
213   return hiddenWindowWidget;
216 void nsCocoaUtils::PrepareForNativeAppModalDialog() {
217   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
219   // Don't do anything if this is embedding. We'll assume that if there is no hidden
220   // window we shouldn't do anything, and that should cover the embedding case.
221   nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
222   if (!hiddenWindowMenuBar) return;
224   // First put up the hidden window menu bar so that app menu event handling is correct.
225   hiddenWindowMenuBar->Paint();
227   NSMenu* mainMenu = [NSApp mainMenu];
228   NS_ASSERTION([mainMenu numberOfItems] > 0,
229                "Main menu does not have any items, something is terribly wrong!");
231   // Create new menu bar for use with modal dialog
232   NSMenu* newMenuBar = [[NSMenu alloc] initWithTitle:@""];
234   // Swap in our app menu. Note that the event target is whatever window is up when
235   // the app modal dialog goes up.
236   NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain];
237   [mainMenu removeItemAtIndex:0];
238   [newMenuBar insertItem:firstMenuItem atIndex:0];
239   [firstMenuItem release];
241   // Add standard edit menu
242   [newMenuBar addItem:nsMenuUtilsX::GetStandardEditMenuItem()];
244   // Show the new menu bar
245   [NSApp setMainMenu:newMenuBar];
246   [newMenuBar release];
248   NS_OBJC_END_TRY_IGNORE_BLOCK;
251 void nsCocoaUtils::CleanUpAfterNativeAppModalDialog() {
252   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
254   // Don't do anything if this is embedding. We'll assume that if there is no hidden
255   // window we shouldn't do anything, and that should cover the embedding case.
256   nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
257   if (!hiddenWindowMenuBar) return;
259   NSWindow* mainWindow = [NSApp mainWindow];
260   if (!mainWindow)
261     hiddenWindowMenuBar->Paint();
262   else
263     [WindowDelegate paintMenubarForWindow:mainWindow];
265   NS_OBJC_END_TRY_IGNORE_BLOCK;
268 static void data_ss_release_callback(void* aDataSourceSurface, const void* data, size_t size) {
269   if (aDataSourceSurface) {
270     static_cast<DataSourceSurface*>(aDataSourceSurface)->Unmap();
271     static_cast<DataSourceSurface*>(aDataSourceSurface)->Release();
272   }
275 // This function assumes little endian byte order.
276 static bool ComputeIsEntirelyBlack(const DataSourceSurface::MappedSurface& aMap,
277                                    const IntSize& aSize) {
278   for (int32_t y = 0; y < aSize.height; y++) {
279     size_t rowStart = y * aMap.mStride;
280     for (int32_t x = 0; x < aSize.width; x++) {
281       size_t index = rowStart + x * 4;
282       if (aMap.mData[index + 0] != 0 || aMap.mData[index + 1] != 0 || aMap.mData[index + 2] != 0) {
283         return false;
284       }
285     }
286   }
287   return true;
290 nsresult nsCocoaUtils::CreateCGImageFromSurface(SourceSurface* aSurface, CGImageRef* aResult,
291                                                 bool* aIsEntirelyBlack) {
292   RefPtr<DataSourceSurface> dataSurface;
294   if (aSurface->GetFormat() == SurfaceFormat::B8G8R8A8) {
295     dataSurface = aSurface->GetDataSurface();
296   } else {
297     // CGImageCreate only supports 16- and 32-bit bit-depth
298     // Convert format to SurfaceFormat::B8G8R8A8
299     dataSurface =
300         gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat(aSurface, SurfaceFormat::B8G8R8A8);
301   }
303   NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
305   int32_t width = dataSurface->GetSize().width;
306   int32_t height = dataSurface->GetSize().height;
307   if (height < 1 || width < 1) {
308     return NS_ERROR_FAILURE;
309   }
311   DataSourceSurface::MappedSurface map;
312   if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
313     return NS_ERROR_FAILURE;
314   }
315   // The Unmap() call happens in data_ss_release_callback
317   if (aIsEntirelyBlack) {
318     *aIsEntirelyBlack = ComputeIsEntirelyBlack(map, dataSurface->GetSize());
319   }
321   // Create a CGImageRef with the bits from the image, taking into account
322   // the alpha ordering and endianness of the machine so we don't have to
323   // touch the bits ourselves.
324   CGDataProviderRef dataProvider = ::CGDataProviderCreateWithData(
325       dataSurface.forget().take(), map.mData, map.mStride * height, data_ss_release_callback);
326   CGColorSpaceRef colorSpace = ::CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
327   *aResult = ::CGImageCreate(width, height, 8, 32, map.mStride, colorSpace,
328                              kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst,
329                              dataProvider, NULL, 0, kCGRenderingIntentDefault);
330   ::CGColorSpaceRelease(colorSpace);
331   ::CGDataProviderRelease(dataProvider);
332   return *aResult ? NS_OK : NS_ERROR_FAILURE;
335 nsresult nsCocoaUtils::CreateNSImageFromCGImage(CGImageRef aInputImage, NSImage** aResult) {
336   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
338   // Be very careful when creating the NSImage that the backing NSImageRep is
339   // exactly 1:1 with the input image. On a retina display, both [NSImage
340   // lockFocus] and [NSImage initWithCGImage:size:] will create an image with a
341   // 2x backing NSImageRep. This prevents NSCursor from recognizing a retina
342   // cursor, which only occurs if pixelsWide and pixelsHigh are exactly 2x the
343   // size of the NSImage.
344   //
345   // For example, if a 32x32 SVG cursor is rendered on a retina display, then
346   // aInputImage will be 64x64. The resulting NSImage will be scaled back down
347   // to 32x32 so it stays the correct size on the screen by changing its size
348   // (resizing a NSImage only scales the image and doesn't resample the data).
349   // If aInputImage is converted using [NSImage initWithCGImage:size:] then the
350   // bitmap will be 128x128 and NSCursor won't recognize a retina cursor, since
351   // it will expect a 64x64 bitmap.
353   int32_t width = ::CGImageGetWidth(aInputImage);
354   int32_t height = ::CGImageGetHeight(aInputImage);
355   NSRect imageRect = ::NSMakeRect(0.0, 0.0, width, height);
357   NSBitmapImageRep* offscreenRep =
358       [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
359                                               pixelsWide:width
360                                               pixelsHigh:height
361                                            bitsPerSample:8
362                                          samplesPerPixel:4
363                                                 hasAlpha:YES
364                                                 isPlanar:NO
365                                           colorSpaceName:NSDeviceRGBColorSpace
366                                             bitmapFormat:NSAlphaFirstBitmapFormat
367                                              bytesPerRow:0
368                                             bitsPerPixel:0];
370   NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
371   [NSGraphicsContext saveGraphicsState];
372   [NSGraphicsContext setCurrentContext:context];
374   // Get the Quartz context and draw.
375   CGContextRef imageContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
376   ::CGContextDrawImage(imageContext, *(CGRect*)&imageRect, aInputImage);
378   [NSGraphicsContext restoreGraphicsState];
380   *aResult = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
381   [*aResult addRepresentation:offscreenRep];
382   [offscreenRep release];
383   return NS_OK;
385   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
388 nsresult nsCocoaUtils::CreateNSImageFromImageContainer(imgIContainer* aImage, uint32_t aWhichFrame,
389                                                        const nsPresContext* aPresContext,
390                                                        const ComputedStyle* aComputedStyle,
391                                                        NSImage** aResult, CGFloat scaleFactor,
392                                                        bool* aIsEntirelyBlack) {
393   RefPtr<SourceSurface> surface;
394   int32_t width = 0, height = 0;
395   aImage->GetWidth(&width);
396   aImage->GetHeight(&height);
398   // Render a vector image at the correct resolution on a retina display
399   if (aImage->GetType() == imgIContainer::TYPE_VECTOR) {
400     IntSize scaledSize = IntSize::Ceil(width * scaleFactor, height * scaleFactor);
402     RefPtr<DrawTarget> drawTarget = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
403         scaledSize, SurfaceFormat::B8G8R8A8);
404     if (!drawTarget || !drawTarget->IsValid()) {
405       NS_ERROR("Failed to create valid DrawTarget");
406       return NS_ERROR_FAILURE;
407     }
409     RefPtr<gfxContext> context = gfxContext::CreateOrNull(drawTarget);
410     MOZ_ASSERT(context);
412     SVGImageContext svgContext;
413     if (aPresContext && aComputedStyle) {
414       SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, *aComputedStyle, aImage);
415     }
416     mozilla::image::ImgDrawResult res =
417         aImage->Draw(context, scaledSize, ImageRegion::Create(scaledSize), aWhichFrame,
418                      SamplingFilter::POINT, svgContext, imgIContainer::FLAG_SYNC_DECODE, 1.0);
420     if (res != mozilla::image::ImgDrawResult::SUCCESS) {
421       return NS_ERROR_FAILURE;
422     }
424     surface = drawTarget->Snapshot();
425   } else {
426     surface = aImage->GetFrame(aWhichFrame,
427                                imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
428   }
430   NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
432   CGImageRef imageRef = NULL;
433   nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef, aIsEntirelyBlack);
434   if (NS_FAILED(rv) || !imageRef) {
435     return NS_ERROR_FAILURE;
436   }
438   rv = nsCocoaUtils::CreateNSImageFromCGImage(imageRef, aResult);
439   if (NS_FAILED(rv) || !aResult) {
440     return NS_ERROR_FAILURE;
441   }
442   ::CGImageRelease(imageRef);
444   // Ensure the image will be rendered the correct size on a retina display
445   NSSize size = NSMakeSize(width, height);
446   [*aResult setSize:size];
447   [[[*aResult representations] objectAtIndex:0] setSize:size];
448   return NS_OK;
451 nsresult nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
452     imgIContainer* aImage, uint32_t aWhichFrame, const nsPresContext* aPresContext,
453     const ComputedStyle* aComputedStyle, NSImage** aResult, bool* aIsEntirelyBlack) {
454   int32_t width = 0, height = 0;
455   aImage->GetWidth(&width);
456   aImage->GetHeight(&height);
457   NSSize size = NSMakeSize(width, height);
458   *aResult = [[NSImage alloc] init];
459   [*aResult setSize:size];
461   NSImage* newRepresentation = nil;
462   nsresult rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aPresContext, aComputedStyle,
463                                                 &newRepresentation, 1.0f, aIsEntirelyBlack);
464   if (NS_FAILED(rv) || !newRepresentation) {
465     return NS_ERROR_FAILURE;
466   }
468   [[[newRepresentation representations] objectAtIndex:0] setSize:size];
469   [*aResult addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
470   [newRepresentation release];
471   newRepresentation = nil;
473   rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aPresContext, aComputedStyle,
474                                        &newRepresentation, 2.0f, aIsEntirelyBlack);
475   if (NS_FAILED(rv) || !newRepresentation) {
476     return NS_ERROR_FAILURE;
477   }
479   [[[newRepresentation representations] objectAtIndex:0] setSize:size];
480   [*aResult addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
481   [newRepresentation release];
482   return NS_OK;
485 // static
486 void nsCocoaUtils::GetStringForNSString(const NSString* aSrc, nsAString& aDist) {
487   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
489   if (!aSrc) {
490     aDist.Truncate();
491     return;
492   }
494   aDist.SetLength([aSrc length]);
495   [aSrc getCharacters:reinterpret_cast<unichar*>(aDist.BeginWriting())
496                 range:NSMakeRange(0, [aSrc length])];
498   NS_OBJC_END_TRY_IGNORE_BLOCK;
501 // static
502 NSString* nsCocoaUtils::ToNSString(const nsAString& aString) {
503   if (aString.IsEmpty()) {
504     return [NSString string];
505   }
506   return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aString.BeginReading())
507                                  length:aString.Length()];
510 // static
511 NSString* nsCocoaUtils::ToNSString(const nsACString& aCString) {
512   if (aCString.IsEmpty()) {
513     return [NSString string];
514   }
515   return [[[NSString alloc] initWithBytes:aCString.BeginReading()
516                                    length:aCString.Length()
517                                  encoding:NSUTF8StringEncoding] autorelease];
520 // static
521 NSURL* nsCocoaUtils::ToNSURL(const nsAString& aURLString) {
522   nsAutoCString encodedURLString;
523   nsresult rv = NS_GetSpecWithNSURLEncoding(encodedURLString, NS_ConvertUTF16toUTF8(aURLString));
524   NS_ENSURE_SUCCESS(rv, nullptr);
526   NSString* encodedURLNSString = ToNSString(encodedURLString);
527   if (!encodedURLNSString) {
528     return nullptr;
529   }
531   return [NSURL URLWithString:encodedURLNSString];
534 // static
535 void nsCocoaUtils::GeckoRectToNSRect(const nsIntRect& aGeckoRect, NSRect& aOutCocoaRect) {
536   aOutCocoaRect.origin.x = aGeckoRect.x;
537   aOutCocoaRect.origin.y = aGeckoRect.y;
538   aOutCocoaRect.size.width = aGeckoRect.width;
539   aOutCocoaRect.size.height = aGeckoRect.height;
542 // static
543 void nsCocoaUtils::NSRectToGeckoRect(const NSRect& aCocoaRect, nsIntRect& aOutGeckoRect) {
544   aOutGeckoRect.x = NSToIntRound(aCocoaRect.origin.x);
545   aOutGeckoRect.y = NSToIntRound(aCocoaRect.origin.y);
546   aOutGeckoRect.width = NSToIntRound(aCocoaRect.origin.x + aCocoaRect.size.width) - aOutGeckoRect.x;
547   aOutGeckoRect.height =
548       NSToIntRound(aCocoaRect.origin.y + aCocoaRect.size.height) - aOutGeckoRect.y;
551 // static
552 NSEvent* nsCocoaUtils::MakeNewCocoaEventWithType(NSEventType aEventType, NSEvent* aEvent) {
553   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
555   NSEvent* newEvent = [NSEvent keyEventWithType:aEventType
556                                        location:[aEvent locationInWindow]
557                                   modifierFlags:[aEvent modifierFlags]
558                                       timestamp:[aEvent timestamp]
559                                    windowNumber:[aEvent windowNumber]
560                                         context:nil
561                                      characters:[aEvent characters]
562                     charactersIgnoringModifiers:[aEvent charactersIgnoringModifiers]
563                                       isARepeat:[aEvent isARepeat]
564                                         keyCode:[aEvent keyCode]];
565   return newEvent;
567   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
570 // static
571 NSEvent* nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(const WidgetKeyboardEvent& aKeyEvent,
572                                                          NSInteger aWindowNumber,
573                                                          NSGraphicsContext* aContext) {
574   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
576   NSEventType eventType;
577   if (aKeyEvent.mMessage == eKeyUp) {
578     eventType = NSEventTypeKeyUp;
579   } else {
580     eventType = NSEventTypeKeyDown;
581   }
583   static const uint32_t sModifierFlagMap[][2] = {{MODIFIER_SHIFT, NSEventModifierFlagShift},
584                                                  {MODIFIER_CONTROL, NSEventModifierFlagControl},
585                                                  {MODIFIER_ALT, NSEventModifierFlagOption},
586                                                  {MODIFIER_ALTGRAPH, NSEventModifierFlagOption},
587                                                  {MODIFIER_META, NSEventModifierFlagCommand},
588                                                  {MODIFIER_CAPSLOCK, NSEventModifierFlagCapsLock},
589                                                  {MODIFIER_NUMLOCK, NSEventModifierFlagNumericPad}};
591   NSUInteger modifierFlags = 0;
592   for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) {
593     if (aKeyEvent.mModifiers & sModifierFlagMap[i][0]) {
594       modifierFlags |= sModifierFlagMap[i][1];
595     }
596   }
598   NSString* characters;
599   if (aKeyEvent.mCharCode) {
600     characters =
601         [NSString stringWithCharacters:reinterpret_cast<const unichar*>(&(aKeyEvent.mCharCode))
602                                 length:1];
603   } else {
604     uint32_t cocoaCharCode = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aKeyEvent.mKeyCode);
605     characters = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(&cocoaCharCode)
606                                          length:1];
607   }
609   return [NSEvent keyEventWithType:eventType
610                           location:NSMakePoint(0, 0)
611                      modifierFlags:modifierFlags
612                          timestamp:0
613                       windowNumber:aWindowNumber
614                            context:aContext
615                         characters:characters
616        charactersIgnoringModifiers:characters
617                          isARepeat:NO
618                            keyCode:0];  // Native key code not currently needed
620   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
623 // static
624 void nsCocoaUtils::InitInputEvent(WidgetInputEvent& aInputEvent, NSEvent* aNativeEvent) {
625   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
627   aInputEvent.mModifiers = ModifiersForEvent(aNativeEvent);
628   aInputEvent.mTime = PR_IntervalNow();
629   aInputEvent.mTimeStamp = GetEventTimeStamp([aNativeEvent timestamp]);
631   NS_OBJC_END_TRY_IGNORE_BLOCK;
634 // static
635 Modifiers nsCocoaUtils::ModifiersForEvent(NSEvent* aNativeEvent) {
636   NSUInteger modifiers = aNativeEvent ? [aNativeEvent modifierFlags] : [NSEvent modifierFlags];
637   Modifiers result = 0;
638   if (modifiers & NSEventModifierFlagShift) {
639     result |= MODIFIER_SHIFT;
640   }
641   if (modifiers & NSEventModifierFlagControl) {
642     result |= MODIFIER_CONTROL;
643   }
644   if (modifiers & NSEventModifierFlagOption) {
645     result |= MODIFIER_ALT;
646     // Mac's option key is similar to other platforms' AltGr key.
647     // Let's set AltGr flag when option key is pressed for consistency with
648     // other platforms.
649     result |= MODIFIER_ALTGRAPH;
650   }
651   if (modifiers & NSEventModifierFlagCommand) {
652     result |= MODIFIER_META;
653   }
655   if (modifiers & NSEventModifierFlagCapsLock) {
656     result |= MODIFIER_CAPSLOCK;
657   }
658   // Mac doesn't have NumLock key.  We can assume that NumLock is always locked
659   // if user is using a keyboard which has numpad.  Otherwise, if user is using
660   // a keyboard which doesn't have numpad, e.g., MacBook's keyboard, we can
661   // assume that NumLock is always unlocked.
662   // Unfortunately, we cannot know whether current keyboard has numpad or not.
663   // We should notify locked state only when keys in numpad are pressed.
664   // By this, web applications may not be confused by unexpected numpad key's
665   // key event with unlocked state.
666   if (modifiers & NSEventModifierFlagNumericPad) {
667     result |= MODIFIER_NUMLOCK;
668   }
670   // Be aware, NSEventModifierFlagFunction is included when arrow keys, home key or some
671   // other keys are pressed. We cannot check whether 'fn' key is pressed or
672   // not by the flag.
674   return result;
677 // static
678 UInt32 nsCocoaUtils::ConvertToCarbonModifier(NSUInteger aCocoaModifier) {
679   UInt32 carbonModifier = 0;
680   if (aCocoaModifier & NSEventModifierFlagCapsLock) {
681     carbonModifier |= alphaLock;
682   }
683   if (aCocoaModifier & NSEventModifierFlagControl) {
684     carbonModifier |= controlKey;
685   }
686   if (aCocoaModifier & NSEventModifierFlagOption) {
687     carbonModifier |= optionKey;
688   }
689   if (aCocoaModifier & NSEventModifierFlagShift) {
690     carbonModifier |= shiftKey;
691   }
692   if (aCocoaModifier & NSEventModifierFlagCommand) {
693     carbonModifier |= cmdKey;
694   }
695   if (aCocoaModifier & NSEventModifierFlagNumericPad) {
696     carbonModifier |= kEventKeyModifierNumLockMask;
697   }
698   if (aCocoaModifier & NSEventModifierFlagFunction) {
699     carbonModifier |= kEventKeyModifierFnMask;
700   }
701   return carbonModifier;
704 // While HiDPI support is not 100% complete and tested, we'll have a pref
705 // to allow it to be turned off in case of problems (or for testing purposes).
707 // gfx.hidpi.enabled is an integer with the meaning:
708 //    <= 0 : HiDPI support is disabled
709 //       1 : HiDPI enabled provided all screens have the same backing resolution
710 //     > 1 : HiDPI enabled even if there are a mixture of screen modes
712 // All the following code is to be removed once HiDPI work is more complete.
714 static bool sHiDPIEnabled = false;
715 static bool sHiDPIPrefInitialized = false;
717 // static
718 bool nsCocoaUtils::HiDPIEnabled() {
719   if (!sHiDPIPrefInitialized) {
720     sHiDPIPrefInitialized = true;
722     int prefSetting = Preferences::GetInt("gfx.hidpi.enabled", 1);
723     if (prefSetting <= 0) {
724       return false;
725     }
727     // prefSetting is at least 1, need to check attached screens...
729     int scaleFactors = 0;  // used as a bitset to track the screen types found
730     NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
731     while (NSScreen* screen = [screenEnum nextObject]) {
732       NSDictionary* desc = [screen deviceDescription];
733       if ([desc objectForKey:NSDeviceIsScreen] == nil) {
734         continue;
735       }
736       // Currently, we only care about differentiating "1.0" and "2.0",
737       // so we set one of the two low bits to record which.
738       if ([screen backingScaleFactor] > 1.0) {
739         scaleFactors |= 2;
740       } else {
741         scaleFactors |= 1;
742       }
743     }
745     // Now scaleFactors will be:
746     //   0 if no screens (supporting backingScaleFactor) found
747     //   1 if only lo-DPI screens
748     //   2 if only hi-DPI screens
749     //   3 if both lo- and hi-DPI screens
750     // We'll enable HiDPI support if there's only a single screen type,
751     // OR if the pref setting is explicitly greater than 1.
752     sHiDPIEnabled = (scaleFactors <= 2) || (prefSetting > 1);
753   }
755   return sHiDPIEnabled;
758 // static
759 void nsCocoaUtils::InvalidateHiDPIState() { sHiDPIPrefInitialized = false; }
761 void nsCocoaUtils::GetCommandsFromKeyEvent(NSEvent* aEvent,
762                                            nsTArray<KeyBindingsCommand>& aCommands) {
763   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
765   MOZ_ASSERT(aEvent);
767   static NativeKeyBindingsRecorder* sNativeKeyBindingsRecorder;
768   if (!sNativeKeyBindingsRecorder) {
769     sNativeKeyBindingsRecorder = [NativeKeyBindingsRecorder new];
770   }
772   [sNativeKeyBindingsRecorder startRecording:aCommands];
774   // This will trigger 0 - N calls to doCommandBySelector: and insertText:
775   [sNativeKeyBindingsRecorder interpretKeyEvents:[NSArray arrayWithObject:aEvent]];
777   NS_OBJC_END_TRY_IGNORE_BLOCK;
780 @implementation NativeKeyBindingsRecorder
782 - (void)startRecording:(nsTArray<KeyBindingsCommand>&)aCommands {
783   mCommands = &aCommands;
784   mCommands->Clear();
787 - (void)doCommandBySelector:(SEL)aSelector {
788   KeyBindingsCommand command = {aSelector, nil};
790   mCommands->AppendElement(command);
793 - (void)insertText:(id)aString {
794   KeyBindingsCommand command = {@selector(insertText:), aString};
796   mCommands->AppendElement(command);
799 @end  // NativeKeyBindingsRecorder
801 struct KeyConversionData {
802   const char* str;
803   size_t strLength;
804   uint32_t geckoKeyCode;
805   uint32_t charCode;
808 static const KeyConversionData gKeyConversions[] = {
810 #define KEYCODE_ENTRY(aStr, aCode) \
811   { #aStr, sizeof(#aStr) - 1, NS_##aStr, aCode }
813 // Some keycodes may have different name in KeyboardEvent from its key name.
814 #define KEYCODE_ENTRY2(aStr, aNSName, aCode) \
815   { #aStr, sizeof(#aStr) - 1, NS_##aNSName, aCode }
817     KEYCODE_ENTRY(VK_CANCEL, 0x001B),
818     KEYCODE_ENTRY(VK_DELETE, NSDeleteFunctionKey),
819     KEYCODE_ENTRY(VK_BACK, NSBackspaceCharacter),
820     KEYCODE_ENTRY2(VK_BACK_SPACE, VK_BACK, NSBackspaceCharacter),
821     KEYCODE_ENTRY(VK_TAB, NSTabCharacter),
822     KEYCODE_ENTRY(VK_CLEAR, NSClearLineFunctionKey),
823     KEYCODE_ENTRY(VK_RETURN, NSEnterCharacter),
824     KEYCODE_ENTRY(VK_SHIFT, 0),
825     KEYCODE_ENTRY(VK_CONTROL, 0),
826     KEYCODE_ENTRY(VK_ALT, 0),
827     KEYCODE_ENTRY(VK_PAUSE, NSPauseFunctionKey),
828     KEYCODE_ENTRY(VK_CAPS_LOCK, 0),
829     KEYCODE_ENTRY(VK_ESCAPE, 0),
830     KEYCODE_ENTRY(VK_SPACE, ' '),
831     KEYCODE_ENTRY(VK_PAGE_UP, NSPageUpFunctionKey),
832     KEYCODE_ENTRY(VK_PAGE_DOWN, NSPageDownFunctionKey),
833     KEYCODE_ENTRY(VK_END, NSEndFunctionKey),
834     KEYCODE_ENTRY(VK_HOME, NSHomeFunctionKey),
835     KEYCODE_ENTRY(VK_LEFT, NSLeftArrowFunctionKey),
836     KEYCODE_ENTRY(VK_UP, NSUpArrowFunctionKey),
837     KEYCODE_ENTRY(VK_RIGHT, NSRightArrowFunctionKey),
838     KEYCODE_ENTRY(VK_DOWN, NSDownArrowFunctionKey),
839     KEYCODE_ENTRY(VK_PRINTSCREEN, NSPrintScreenFunctionKey),
840     KEYCODE_ENTRY(VK_INSERT, NSInsertFunctionKey),
841     KEYCODE_ENTRY(VK_HELP, NSHelpFunctionKey),
842     KEYCODE_ENTRY(VK_0, '0'),
843     KEYCODE_ENTRY(VK_1, '1'),
844     KEYCODE_ENTRY(VK_2, '2'),
845     KEYCODE_ENTRY(VK_3, '3'),
846     KEYCODE_ENTRY(VK_4, '4'),
847     KEYCODE_ENTRY(VK_5, '5'),
848     KEYCODE_ENTRY(VK_6, '6'),
849     KEYCODE_ENTRY(VK_7, '7'),
850     KEYCODE_ENTRY(VK_8, '8'),
851     KEYCODE_ENTRY(VK_9, '9'),
852     KEYCODE_ENTRY(VK_SEMICOLON, ':'),
853     KEYCODE_ENTRY(VK_EQUALS, '='),
854     KEYCODE_ENTRY(VK_A, 'A'),
855     KEYCODE_ENTRY(VK_B, 'B'),
856     KEYCODE_ENTRY(VK_C, 'C'),
857     KEYCODE_ENTRY(VK_D, 'D'),
858     KEYCODE_ENTRY(VK_E, 'E'),
859     KEYCODE_ENTRY(VK_F, 'F'),
860     KEYCODE_ENTRY(VK_G, 'G'),
861     KEYCODE_ENTRY(VK_H, 'H'),
862     KEYCODE_ENTRY(VK_I, 'I'),
863     KEYCODE_ENTRY(VK_J, 'J'),
864     KEYCODE_ENTRY(VK_K, 'K'),
865     KEYCODE_ENTRY(VK_L, 'L'),
866     KEYCODE_ENTRY(VK_M, 'M'),
867     KEYCODE_ENTRY(VK_N, 'N'),
868     KEYCODE_ENTRY(VK_O, 'O'),
869     KEYCODE_ENTRY(VK_P, 'P'),
870     KEYCODE_ENTRY(VK_Q, 'Q'),
871     KEYCODE_ENTRY(VK_R, 'R'),
872     KEYCODE_ENTRY(VK_S, 'S'),
873     KEYCODE_ENTRY(VK_T, 'T'),
874     KEYCODE_ENTRY(VK_U, 'U'),
875     KEYCODE_ENTRY(VK_V, 'V'),
876     KEYCODE_ENTRY(VK_W, 'W'),
877     KEYCODE_ENTRY(VK_X, 'X'),
878     KEYCODE_ENTRY(VK_Y, 'Y'),
879     KEYCODE_ENTRY(VK_Z, 'Z'),
880     KEYCODE_ENTRY(VK_CONTEXT_MENU, NSMenuFunctionKey),
881     KEYCODE_ENTRY(VK_NUMPAD0, '0'),
882     KEYCODE_ENTRY(VK_NUMPAD1, '1'),
883     KEYCODE_ENTRY(VK_NUMPAD2, '2'),
884     KEYCODE_ENTRY(VK_NUMPAD3, '3'),
885     KEYCODE_ENTRY(VK_NUMPAD4, '4'),
886     KEYCODE_ENTRY(VK_NUMPAD5, '5'),
887     KEYCODE_ENTRY(VK_NUMPAD6, '6'),
888     KEYCODE_ENTRY(VK_NUMPAD7, '7'),
889     KEYCODE_ENTRY(VK_NUMPAD8, '8'),
890     KEYCODE_ENTRY(VK_NUMPAD9, '9'),
891     KEYCODE_ENTRY(VK_MULTIPLY, '*'),
892     KEYCODE_ENTRY(VK_ADD, '+'),
893     KEYCODE_ENTRY(VK_SEPARATOR, 0),
894     KEYCODE_ENTRY(VK_SUBTRACT, '-'),
895     KEYCODE_ENTRY(VK_DECIMAL, '.'),
896     KEYCODE_ENTRY(VK_DIVIDE, '/'),
897     KEYCODE_ENTRY(VK_F1, NSF1FunctionKey),
898     KEYCODE_ENTRY(VK_F2, NSF2FunctionKey),
899     KEYCODE_ENTRY(VK_F3, NSF3FunctionKey),
900     KEYCODE_ENTRY(VK_F4, NSF4FunctionKey),
901     KEYCODE_ENTRY(VK_F5, NSF5FunctionKey),
902     KEYCODE_ENTRY(VK_F6, NSF6FunctionKey),
903     KEYCODE_ENTRY(VK_F7, NSF7FunctionKey),
904     KEYCODE_ENTRY(VK_F8, NSF8FunctionKey),
905     KEYCODE_ENTRY(VK_F9, NSF9FunctionKey),
906     KEYCODE_ENTRY(VK_F10, NSF10FunctionKey),
907     KEYCODE_ENTRY(VK_F11, NSF11FunctionKey),
908     KEYCODE_ENTRY(VK_F12, NSF12FunctionKey),
909     KEYCODE_ENTRY(VK_F13, NSF13FunctionKey),
910     KEYCODE_ENTRY(VK_F14, NSF14FunctionKey),
911     KEYCODE_ENTRY(VK_F15, NSF15FunctionKey),
912     KEYCODE_ENTRY(VK_F16, NSF16FunctionKey),
913     KEYCODE_ENTRY(VK_F17, NSF17FunctionKey),
914     KEYCODE_ENTRY(VK_F18, NSF18FunctionKey),
915     KEYCODE_ENTRY(VK_F19, NSF19FunctionKey),
916     KEYCODE_ENTRY(VK_F20, NSF20FunctionKey),
917     KEYCODE_ENTRY(VK_F21, NSF21FunctionKey),
918     KEYCODE_ENTRY(VK_F22, NSF22FunctionKey),
919     KEYCODE_ENTRY(VK_F23, NSF23FunctionKey),
920     KEYCODE_ENTRY(VK_F24, NSF24FunctionKey),
921     KEYCODE_ENTRY(VK_NUM_LOCK, NSClearLineFunctionKey),
922     KEYCODE_ENTRY(VK_SCROLL_LOCK, NSScrollLockFunctionKey),
923     KEYCODE_ENTRY(VK_COMMA, ','),
924     KEYCODE_ENTRY(VK_PERIOD, '.'),
925     KEYCODE_ENTRY(VK_SLASH, '/'),
926     KEYCODE_ENTRY(VK_BACK_QUOTE, '`'),
927     KEYCODE_ENTRY(VK_OPEN_BRACKET, '['),
928     KEYCODE_ENTRY(VK_BACK_SLASH, '\\'),
929     KEYCODE_ENTRY(VK_CLOSE_BRACKET, ']'),
930     KEYCODE_ENTRY(VK_QUOTE, '\'')
932 #undef KEYCODE_ENTRY
936 uint32_t nsCocoaUtils::ConvertGeckoNameToMacCharCode(const nsAString& aKeyCodeName) {
937   if (aKeyCodeName.IsEmpty()) {
938     return 0;
939   }
941   nsAutoCString keyCodeName;
942   LossyCopyUTF16toASCII(aKeyCodeName, keyCodeName);
943   // We want case-insensitive comparison with data stored as uppercase.
944   ToUpperCase(keyCodeName);
946   uint32_t keyCodeNameLength = keyCodeName.Length();
947   const char* keyCodeNameStr = keyCodeName.get();
948   for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
949     if (keyCodeNameLength == gKeyConversions[i].strLength &&
950         nsCRT::strcmp(gKeyConversions[i].str, keyCodeNameStr) == 0) {
951       return gKeyConversions[i].charCode;
952     }
953   }
955   return 0;
958 uint32_t nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(uint32_t aKeyCode) {
959   if (!aKeyCode) {
960     return 0;
961   }
963   for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
964     if (gKeyConversions[i].geckoKeyCode == aKeyCode) {
965       return gKeyConversions[i].charCode;
966     }
967   }
969   return 0;
972 NSEventModifierFlags nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
973     nsIWidget::Modifiers aNativeModifiers) {
974   if (!aNativeModifiers) {
975     return 0;
976   }
977   struct ModifierFlagMapEntry {
978     nsIWidget::Modifiers mWidgetModifier;
979     NSEventModifierFlags mModifierFlags;
980   };
981   static constexpr ModifierFlagMapEntry sModifierFlagMap[] = {
982       {nsIWidget::CAPS_LOCK, NSEventModifierFlagCapsLock},
983       {nsIWidget::SHIFT_L, NSEventModifierFlagShift | 0x0002},
984       {nsIWidget::SHIFT_R, NSEventModifierFlagShift | 0x0004},
985       {nsIWidget::CTRL_L, NSEventModifierFlagControl | 0x0001},
986       {nsIWidget::CTRL_R, NSEventModifierFlagControl | 0x2000},
987       {nsIWidget::ALT_L, NSEventModifierFlagOption | 0x0020},
988       {nsIWidget::ALT_R, NSEventModifierFlagOption | 0x0040},
989       {nsIWidget::COMMAND_L, NSEventModifierFlagCommand | 0x0008},
990       {nsIWidget::COMMAND_R, NSEventModifierFlagCommand | 0x0010},
991       {nsIWidget::NUMERIC_KEY_PAD, NSEventModifierFlagNumericPad},
992       {nsIWidget::HELP, NSEventModifierFlagHelp},
993       {nsIWidget::FUNCTION, NSEventModifierFlagFunction}};
995   NSEventModifierFlags modifierFlags = 0;
996   for (const ModifierFlagMapEntry& entry : sModifierFlagMap) {
997     if (aNativeModifiers & entry.mWidgetModifier) {
998       modifierFlags |= entry.mModifierFlags;
999     }
1000   }
1001   return modifierFlags;
1004 mozilla::MouseButton nsCocoaUtils::ButtonForEvent(NSEvent* aEvent) {
1005   switch (aEvent.type) {
1006     case NSEventTypeLeftMouseDown:
1007     case NSEventTypeLeftMouseDragged:
1008     case NSEventTypeLeftMouseUp:
1009       return MouseButton::ePrimary;
1010     case NSEventTypeRightMouseDown:
1011     case NSEventTypeRightMouseDragged:
1012     case NSEventTypeRightMouseUp:
1013       return MouseButton::eSecondary;
1014     case NSEventTypeOtherMouseDown:
1015     case NSEventTypeOtherMouseDragged:
1016     case NSEventTypeOtherMouseUp:
1017       switch (aEvent.buttonNumber) {
1018         case 3:
1019           return MouseButton::eX1;
1020         case 4:
1021           return MouseButton::eX2;
1022         default:
1023           // The middle button usually has button 2, but if this is a synthesized event (for which
1024           // you cannot specify a buttonNumber), then the button will be 0. Treat all remaining
1025           // OtherMouse events as the middle button.
1026           return MouseButton::eMiddle;
1027       }
1028     default:
1029       // Treat non-mouse events as the primary mouse button.
1030       return MouseButton::ePrimary;
1031   }
1034 NSMutableAttributedString* nsCocoaUtils::GetNSMutableAttributedString(
1035     const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRanges, const bool aIsVertical,
1036     const CGFloat aBackingScaleFactor) {
1037   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
1039   NSString* nsstr = nsCocoaUtils::ToNSString(aText);
1040   NSMutableAttributedString* attrStr =
1041       [[[NSMutableAttributedString alloc] initWithString:nsstr attributes:nil] autorelease];
1043   int32_t lastOffset = aText.Length();
1044   for (auto i = aFontRanges.Length(); i > 0; --i) {
1045     const FontRange& fontRange = aFontRanges[i - 1];
1046     NSString* fontName = nsCocoaUtils::ToNSString(fontRange.mFontName);
1047     CGFloat fontSize = fontRange.mFontSize / aBackingScaleFactor;
1048     NSFont* font = [NSFont fontWithName:fontName size:fontSize];
1049     if (!font) {
1050       font = [NSFont systemFontOfSize:fontSize];
1051     }
1053     NSDictionary* attrs = @{NSFontAttributeName : font};
1054     NSRange range = NSMakeRange(fontRange.mStartOffset, lastOffset - fontRange.mStartOffset);
1055     [attrStr setAttributes:attrs range:range];
1056     lastOffset = fontRange.mStartOffset;
1057   }
1059   if (aIsVertical) {
1060     [attrStr addAttribute:NSVerticalGlyphFormAttributeName
1061                     value:[NSNumber numberWithInt:1]
1062                     range:NSMakeRange(0, [attrStr length])];
1063   }
1065   return attrStr;
1067   NS_OBJC_END_TRY_BLOCK_RETURN(nil)
1070 TimeStamp nsCocoaUtils::GetEventTimeStamp(NSTimeInterval aEventTime) {
1071   if (!aEventTime) {
1072     // If the event is generated by a 3rd party application, its timestamp
1073     // may be 0.  In this case, just return current timestamp.
1074     // XXX Should we cache last event time?
1075     return TimeStamp::Now();
1076   }
1077   // The internal value of the macOS implementation of TimeStamp is based on
1078   // mach_absolute_time(), which measures "ticks" since boot.
1079   // Event timestamps are NSTimeIntervals (seconds) since boot. So the two time
1080   // representations already have the same base; we only need to convert
1081   // seconds into ticks.
1082   int64_t tick = BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aEventTime * 1000.0);
1083   return TimeStamp::FromSystemTime(tick);
1086 static NSString* ActionOnDoubleClickSystemPref() {
1087   NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
1088   NSString* kAppleActionOnDoubleClickKey = @"AppleActionOnDoubleClick";
1089   id value = [userDefaults objectForKey:kAppleActionOnDoubleClickKey];
1090   if ([value isKindOfClass:[NSString class]]) {
1091     return value;
1092   }
1093   return nil;
1096 @interface NSWindow (NSWindowShouldZoomOnDoubleClick)
1097 + (BOOL)_shouldZoomOnDoubleClick;  // present on 10.7 and above
1098 @end
1100 bool nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick() {
1101   if ([NSWindow respondsToSelector:@selector(_shouldZoomOnDoubleClick)]) {
1102     return [NSWindow _shouldZoomOnDoubleClick];
1103   }
1104   return [ActionOnDoubleClickSystemPref() isEqualToString:@"Maximize"];
1107 bool nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick() {
1108   // Check the system preferences.
1109   // We could also check -[NSWindow _shouldMiniaturizeOnDoubleClick]. It's not clear to me which
1110   // approach would be preferable; neither is public API.
1111   return [ActionOnDoubleClickSystemPref() isEqualToString:@"Minimize"];
1114 // AVAuthorizationStatus is not needed unless we are running on 10.14.
1115 // However, on pre-10.14 SDK's, AVAuthorizationStatus and its enum values
1116 // are both defined and prohibited from use by compile-time checks. We
1117 // define a copy of AVAuthorizationStatus to allow compilation on pre-10.14
1118 // SDK's. The enum values must match what is defined in the 10.14 SDK.
1119 // We use ASSERTS for 10.14 SDK builds to check the enum values match.
1120 enum GeckoAVAuthorizationStatus : NSInteger {
1121   GeckoAVAuthorizationStatusNotDetermined = 0,
1122   GeckoAVAuthorizationStatusRestricted = 1,
1123   GeckoAVAuthorizationStatusDenied = 2,
1124   GeckoAVAuthorizationStatusAuthorized = 3
1127 #if !defined(MAC_OS_X_VERSION_10_14) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_14
1128 // Define authorizationStatusForMediaType: as returning
1129 // GeckoAVAuthorizationStatus instead of AVAuthorizationStatus to allow
1130 // compilation on pre-10.14 SDK's.
1131 @interface AVCaptureDevice (GeckoAVAuthorizationStatus)
1132 + (GeckoAVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType;
1133 @end
1135 @interface AVCaptureDevice (WithCompletionHandler)
1136 + (void)requestAccessForMediaType:(AVMediaType)mediaType
1137                 completionHandler:(void (^)(BOOL granted))handler;
1138 @end
1139 #endif
1141 static const char* AVMediaTypeToString(AVMediaType aType) {
1142   if (aType == AVMediaTypeVideo) {
1143     return "video";
1144   }
1146   if (aType == AVMediaTypeAudio) {
1147     return "audio";
1148   }
1150   return "unexpected type";
1153 static void LogAuthorizationStatus(AVMediaType aType, int aState) {
1154   const char* stateString;
1156   switch (aState) {
1157     case GeckoAVAuthorizationStatusAuthorized:
1158       stateString = "AVAuthorizationStatusAuthorized";
1159       break;
1160     case GeckoAVAuthorizationStatusDenied:
1161       stateString = "AVAuthorizationStatusDenied";
1162       break;
1163     case GeckoAVAuthorizationStatusNotDetermined:
1164       stateString = "AVAuthorizationStatusNotDetermined";
1165       break;
1166     case GeckoAVAuthorizationStatusRestricted:
1167       stateString = "AVAuthorizationStatusRestricted";
1168       break;
1169     default:
1170       stateString = "Invalid state";
1171   }
1173   LOG("%s authorization status: %s\n", AVMediaTypeToString(aType), stateString);
1176 static nsresult GetPermissionState(AVMediaType aMediaType, uint16_t& aState) {
1177   MOZ_ASSERT(aMediaType == AVMediaTypeVideo || aMediaType == AVMediaTypeAudio);
1179   // Only attempt to check authorization status on 10.14+.
1180   if (@available(macOS 10.14, *)) {
1181     GeckoAVAuthorizationStatus authStatus = static_cast<GeckoAVAuthorizationStatus>(
1182         [AVCaptureDevice authorizationStatusForMediaType:aMediaType]);
1183     LogAuthorizationStatus(aMediaType, authStatus);
1185     // Convert GeckoAVAuthorizationStatus to nsIOSPermissionRequest const
1186     switch (authStatus) {
1187       case GeckoAVAuthorizationStatusAuthorized:
1188         aState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
1189         return NS_OK;
1190       case GeckoAVAuthorizationStatusDenied:
1191         aState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
1192         return NS_OK;
1193       case GeckoAVAuthorizationStatusNotDetermined:
1194         aState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
1195         return NS_OK;
1196       case GeckoAVAuthorizationStatusRestricted:
1197         aState = nsIOSPermissionRequest::PERMISSION_STATE_RESTRICTED;
1198         return NS_OK;
1199       default:
1200         MOZ_ASSERT(false, "Invalid authorization status");
1201         return NS_ERROR_UNEXPECTED;
1202     }
1203   }
1204   return NS_ERROR_NOT_IMPLEMENTED;
1207 nsresult nsCocoaUtils::GetVideoCapturePermissionState(uint16_t& aPermissionState) {
1208   return GetPermissionState(AVMediaTypeVideo, aPermissionState);
1211 nsresult nsCocoaUtils::GetAudioCapturePermissionState(uint16_t& aPermissionState) {
1212   return GetPermissionState(AVMediaTypeAudio, aPermissionState);
1215 // Set |aPermissionState| to PERMISSION_STATE_AUTHORIZED if this application
1216 // has already been granted permission to record the screen in macOS Security
1217 // and Privacy system settings. If we do not have permission (because the user
1218 // hasn't yet been asked yet or the user previously denied the prompt), use
1219 // PERMISSION_STATE_DENIED. Returns NS_ERROR_NOT_IMPLEMENTED on macOS 10.14
1220 // and earlier.
1221 nsresult nsCocoaUtils::GetScreenCapturePermissionState(uint16_t& aPermissionState) {
1222   aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
1224   // Only attempt to check screen recording authorization status on 10.15+.
1225   // On earlier macOS versions, screen recording is allowed by default.
1226   if (@available(macOS 10.15, *)) {
1227     if (!StaticPrefs::media_macos_screenrecording_oscheck_enabled()) {
1228       aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
1229       LOG("screen authorization status: authorized (test disabled via pref)");
1230       return NS_OK;
1231     }
1233     // Unlike with camera and microphone capture, there is no support for
1234     // checking the screen recording permission status. Instead, an application
1235     // can use the presence of window names (which are privacy sensitive) in
1236     // the window info list as an indication. The list only includes window
1237     // names if the calling application has been authorized to record the
1238     // screen. We use the window name, window level, and owning PID as
1239     // heuristics to determine if we have screen recording permission.
1240     AutoCFRelease<CFArrayRef> windowArray =
1241         CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
1242     if (!windowArray) {
1243       LOG("GetScreenCapturePermissionState() ERROR: got NULL window info list");
1244       return NS_ERROR_UNEXPECTED;
1245     }
1247     int32_t windowLevelDock = CGWindowLevelForKey(kCGDockWindowLevelKey);
1248     int32_t windowLevelNormal = CGWindowLevelForKey(kCGNormalWindowLevelKey);
1249     LOG("GetScreenCapturePermissionState(): DockWindowLevel: %d, "
1250         "NormalWindowLevel: %d",
1251         windowLevelDock, windowLevelNormal);
1253     int32_t thisPid = [[NSProcessInfo processInfo] processIdentifier];
1255     CFIndex windowCount = CFArrayGetCount(windowArray);
1256     LOG("GetScreenCapturePermissionState() returned %ld windows", windowCount);
1257     if (windowCount == 0) {
1258       return NS_ERROR_UNEXPECTED;
1259     }
1261     for (CFIndex i = 0; i < windowCount; i++) {
1262       CFDictionaryRef windowDict =
1263           reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windowArray, i));
1265       // Get the window owner's PID
1266       int32_t windowOwnerPid = -1;
1267       CFNumberRef windowPidRef =
1268           reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowOwnerPID));
1269       if (!windowPidRef || !CFNumberGetValue(windowPidRef, kCFNumberIntType, &windowOwnerPid)) {
1270         LOG("GetScreenCapturePermissionState() ERROR: failed to get window owner");
1271         continue;
1272       }
1274       // Our own window names are always readable and
1275       // therefore not relevant to the heuristic.
1276       if (thisPid == windowOwnerPid) {
1277         continue;
1278       }
1280       CFStringRef windowName =
1281           reinterpret_cast<CFStringRef>(CFDictionaryGetValue(windowDict, kCGWindowName));
1282       if (!windowName) {
1283         continue;
1284       }
1286       CFNumberRef windowLayerRef =
1287           reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowLayer));
1288       int32_t windowLayer;
1289       if (!windowLayerRef || !CFNumberGetValue(windowLayerRef, kCFNumberIntType, &windowLayer)) {
1290         LOG("GetScreenCapturePermissionState() ERROR: failed to get layer");
1291         continue;
1292       }
1294       // If we have a window name and the window is in the dock or normal window
1295       // level, and for another process, assume we have screen recording access.
1296       LOG("GetScreenCapturePermissionState(): windowLayer: %d", windowLayer);
1297       if (windowLayer == windowLevelDock || windowLayer == windowLevelNormal) {
1298         aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
1299         LOG("screen authorization status: authorized");
1300         return NS_OK;
1301       }
1302     }
1304     aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
1305     LOG("screen authorization status: not authorized");
1306     return NS_OK;
1307   }
1309   LOG("GetScreenCapturePermissionState(): nothing to do, not on 10.15+");
1310   return NS_ERROR_NOT_IMPLEMENTED;
1313 nsresult nsCocoaUtils::RequestVideoCapturePermission(RefPtr<Promise>& aPromise) {
1314   MOZ_ASSERT(NS_IsMainThread());
1315   return nsCocoaUtils::RequestCapturePermission(AVMediaTypeVideo, aPromise, sVideoCapturePromises,
1316                                                 VideoCompletionHandler);
1319 nsresult nsCocoaUtils::RequestAudioCapturePermission(RefPtr<Promise>& aPromise) {
1320   MOZ_ASSERT(NS_IsMainThread());
1321   return nsCocoaUtils::RequestCapturePermission(AVMediaTypeAudio, aPromise, sAudioCapturePromises,
1322                                                 AudioCompletionHandler);
1326 // Stores |aPromise| on |aPromiseList| and starts an asynchronous media
1327 // capture request for the given media type |aType|. If we are already
1328 // waiting for a capture request for this media type, don't start a new
1329 // request. |aHandler| is invoked on an arbitrary dispatch queue when the
1330 // request completes and must resolve any waiting Promises on the main
1331 // thread.
1333 nsresult nsCocoaUtils::RequestCapturePermission(AVMediaType aType, RefPtr<Promise>& aPromise,
1334                                                 PromiseArray& aPromiseList,
1335                                                 void (^aHandler)(BOOL granted)) {
1336   MOZ_ASSERT(aType == AVMediaTypeVideo || aType == AVMediaTypeAudio);
1337 #if defined(MAC_OS_X_VERSION_10_14)
1338   // Ensure our enum constants match. We can only do this when
1339   // compiling on 10.14+ because AVAuthorizationStatus is
1340   // prohibited by preprocessor checks on earlier OS versions.
1341   if (@available(macOS 10.14, *)) {
1342     static_assert(
1343         (int)GeckoAVAuthorizationStatusNotDetermined == (int)AVAuthorizationStatusNotDetermined,
1344         "GeckoAVAuthorizationStatusNotDetermined  does not match");
1345     static_assert((int)GeckoAVAuthorizationStatusRestricted == (int)AVAuthorizationStatusRestricted,
1346                   "GeckoAVAuthorizationStatusRestricted does not match");
1347     static_assert((int)GeckoAVAuthorizationStatusDenied == (int)AVAuthorizationStatusDenied,
1348                   "GeckoAVAuthorizationStatusDenied does not match");
1349     static_assert((int)GeckoAVAuthorizationStatusAuthorized == (int)AVAuthorizationStatusAuthorized,
1350                   "GeckoAVAuthorizationStatusAuthorized does not match");
1351   }
1352 #endif
1353   LOG("RequestCapturePermission(%s)", AVMediaTypeToString(aType));
1355   // Only attempt to request authorization on 10.14+.
1356   if (@available(macOS 10.14, *)) {
1357     sMediaCaptureMutex.Lock();
1359     // Initialize our list of promises on first invocation
1360     if (aPromiseList == nullptr) {
1361       aPromiseList = new nsTArray<RefPtr<Promise>>;
1362       ClearOnShutdown(&aPromiseList);
1363     }
1365     aPromiseList->AppendElement(aPromise);
1366     size_t nPromises = aPromiseList->Length();
1368     sMediaCaptureMutex.Unlock();
1370     LOG("RequestCapturePermission(%s): %ld promise(s) unresolved", AVMediaTypeToString(aType),
1371         nPromises);
1373     // If we had one or more more existing promises waiting to be resolved
1374     // by the completion handler, we don't need to start another request.
1375     if (nPromises > 1) {
1376       return NS_OK;
1377     }
1379     // Start the request
1380     [AVCaptureDevice requestAccessForMediaType:aType completionHandler:aHandler];
1381     return NS_OK;
1382   }
1383   return NS_ERROR_NOT_IMPLEMENTED;
1387 // Audio capture request completion handler. Called from an arbitrary
1388 // dispatch queue.
1390 void (^nsCocoaUtils::AudioCompletionHandler)(BOOL) = ^void(BOOL granted) {
1391   nsCocoaUtils::ResolveAudioCapturePromises(granted);
1395 // Video capture request completion handler. Called from an arbitrary
1396 // dispatch queue.
1398 void (^nsCocoaUtils::VideoCompletionHandler)(BOOL) = ^void(BOOL granted) {
1399   nsCocoaUtils::ResolveVideoCapturePromises(granted);
1402 void nsCocoaUtils::ResolveMediaCapturePromises(bool aGranted, PromiseArray& aPromiseList) {
1403   StaticMutexAutoLock lock(sMediaCaptureMutex);
1405   // Remove each promise from the list and resolve it.
1406   while (aPromiseList->Length() > 0) {
1407     RefPtr<Promise> promise = aPromiseList->PopLastElement();
1409     // Resolve on main thread
1410     nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
1411         "ResolveMediaAccessPromise",
1412         [aGranted, aPromise = std::move(promise)]() { aPromise->MaybeResolve(aGranted); }));
1413     NS_DispatchToMainThread(runnable.forget());
1414   }
1417 void nsCocoaUtils::ResolveAudioCapturePromises(bool aGranted) {
1418   // Resolve on main thread
1419   nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction("ResolveAudioCapturePromise", [aGranted]() {
1420     ResolveMediaCapturePromises(aGranted, sAudioCapturePromises);
1421   }));
1422   NS_DispatchToMainThread(runnable.forget());
1426 // Attempt to trigger a dialog requesting permission to record the screen.
1427 // Unlike with the camera and microphone, there is no API to request permission
1428 // to record the screen or to receive a callback when permission is explicitly
1429 // allowed or denied. Here we attempt to trigger the dialog by attempting to
1430 // capture a 1x1 pixel section of the screen. The permission dialog is not
1431 // guaranteed to be displayed because the user may have already been prompted
1432 // in which case macOS does not display the dialog again.
1434 nsresult nsCocoaUtils::MaybeRequestScreenCapturePermission() {
1435   LOG("MaybeRequestScreenCapturePermission()");
1436   AutoCFRelease<CGImageRef> image =
1437       CGDisplayCreateImageForRect(kCGDirectMainDisplay, CGRectMake(0, 0, 1, 1));
1438   return NS_OK;
1441 void nsCocoaUtils::ResolveVideoCapturePromises(bool aGranted) {
1442   // Resolve on main thread
1443   nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction("ResolveVideoCapturePromise", [aGranted]() {
1444     ResolveMediaCapturePromises(aGranted, sVideoCapturePromises);
1445   }));
1446   NS_DispatchToMainThread(runnable.forget());