summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsCocoaUtils.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsCocoaUtils.mm')
-rw-r--r--widget/cocoa/nsCocoaUtils.mm1819
1 files changed, 1819 insertions, 0 deletions
diff --git a/widget/cocoa/nsCocoaUtils.mm b/widget/cocoa/nsCocoaUtils.mm
new file mode 100644
index 0000000000..b6eaa5dc31
--- /dev/null
+++ b/widget/cocoa/nsCocoaUtils.mm
@@ -0,0 +1,1819 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <AVFoundation/AVFoundation.h>
+
+#include <cmath>
+
+#include "AppleUtils.h"
+#include "gfx2DGlue.h"
+#include "gfxContext.h"
+#include "gfxPlatform.h"
+#include "gfxUtils.h"
+#include "ImageRegion.h"
+#include "nsClipboard.h"
+#include "nsCocoaUtils.h"
+#include "nsChildView.h"
+#include "nsMenuBarX.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaWindow.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAppShellService.h"
+#include "nsIOSPermissionRequest.h"
+#include "nsIRunnable.h"
+#include "nsIAppWindow.h"
+#include "nsIBaseWindow.h"
+#include "nsITransferable.h"
+#include "nsMenuUtilsX.h"
+#include "nsNetUtil.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsToolkit.h"
+#include "nsCRT.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SVGImageContext.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/2D.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+using mozilla::dom::Promise;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::DrawTarget;
+using mozilla::gfx::IntPoint;
+using mozilla::gfx::IntRect;
+using mozilla::gfx::IntSize;
+using mozilla::gfx::SamplingFilter;
+using mozilla::gfx::SourceSurface;
+using mozilla::gfx::SurfaceFormat;
+using mozilla::image::ImageRegion;
+
+LazyLogModule gCocoaUtilsLog("nsCocoaUtils");
+#undef LOG
+#define LOG(...) MOZ_LOG(gCocoaUtilsLog, LogLevel::Debug, (__VA_ARGS__))
+
+/*
+ * For each audio and video capture request, we hold an owning reference
+ * to a promise to be resolved when the request's async callback is invoked.
+ * sVideoCapturePromises and sAudioCapturePromises are arrays of video and
+ * audio promises waiting for to be resolved. Each array is protected by a
+ * mutex.
+ */
+nsCocoaUtils::PromiseArray nsCocoaUtils::sVideoCapturePromises;
+nsCocoaUtils::PromiseArray nsCocoaUtils::sAudioCapturePromises;
+StaticMutex nsCocoaUtils::sMediaCaptureMutex;
+
+/**
+ * Pasteboard types
+ */
+NSString* const kPublicUrlPboardType = @"public.url";
+NSString* const kPublicUrlNamePboardType = @"public.url-name";
+NSString* const kUrlsWithTitlesPboardType = @"WebURLsWithTitlesPboardType";
+NSString* const kMozWildcardPboardType = @"org.mozilla.MozillaWildcard";
+NSString* const kMozCustomTypesPboardType = @"org.mozilla.custom-clipdata";
+NSString* const kMozFileUrlsPboardType = @"org.mozilla.file-urls";
+
+@implementation UTIHelper
+
++ (NSString*)stringFromPboardType:(NSString*)aType {
+ if ([aType isEqualToString:kMozWildcardPboardType] ||
+ [aType isEqualToString:kMozCustomTypesPboardType] ||
+ [aType isEqualToString:kPublicUrlPboardType] ||
+ [aType isEqualToString:kPublicUrlNamePboardType] ||
+ [aType isEqualToString:kMozFileUrlsPboardType] ||
+ [aType isEqualToString:(NSString*)kPasteboardTypeFileURLPromise] ||
+ [aType isEqualToString:(NSString*)kPasteboardTypeFilePromiseContent] ||
+ [aType isEqualToString:(NSString*)kUTTypeFileURL] ||
+ [aType isEqualToString:NSStringPboardType] ||
+ [aType isEqualToString:NSPasteboardTypeString] ||
+ [aType isEqualToString:NSPasteboardTypeHTML] || [aType isEqualToString:NSPasteboardTypeRTF] ||
+ [aType isEqualToString:NSPasteboardTypeTIFF] || [aType isEqualToString:NSPasteboardTypePNG]) {
+ return [NSString stringWithString:aType];
+ }
+ NSString* dynamicType = (NSString*)UTTypeCreatePreferredIdentifierForTag(
+ kUTTagClassNSPboardType, (CFStringRef)aType, kUTTypeData);
+ NSString* result = [NSString stringWithString:dynamicType];
+ [dynamicType release];
+ return result;
+}
+
+@end // UTIHelper
+
+static float MenuBarScreenHeight() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSArray* allScreens = [NSScreen screens];
+ if ([allScreens count]) {
+ return [[allScreens objectAtIndex:0] frame].size.height;
+ }
+
+ return 0.0;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0.0);
+}
+
+float nsCocoaUtils::FlippedScreenY(float y) { return MenuBarScreenHeight() - y; }
+
+NSRect nsCocoaUtils::GeckoRectToCocoaRect(const DesktopIntRect& geckoRect) {
+ // We only need to change the Y coordinate by starting with the primary screen
+ // height and subtracting the gecko Y coordinate of the bottom of the rect.
+ return NSMakeRect(geckoRect.x, MenuBarScreenHeight() - geckoRect.YMost(), geckoRect.width,
+ geckoRect.height);
+}
+
+NSPoint nsCocoaUtils::GeckoPointToCocoaPoint(const mozilla::DesktopPoint& aPoint) {
+ return NSMakePoint(aPoint.x, MenuBarScreenHeight() - aPoint.y);
+}
+
+NSRect nsCocoaUtils::GeckoRectToCocoaRectDevPix(const LayoutDeviceIntRect& aGeckoRect,
+ CGFloat aBackingScale) {
+ return NSMakeRect(aGeckoRect.x / aBackingScale,
+ MenuBarScreenHeight() - aGeckoRect.YMost() / aBackingScale,
+ aGeckoRect.width / aBackingScale, aGeckoRect.height / aBackingScale);
+}
+
+DesktopIntRect nsCocoaUtils::CocoaRectToGeckoRect(const NSRect& cocoaRect) {
+ // We only need to change the Y coordinate by starting with the primary screen
+ // height and subtracting both the cocoa y origin and the height of the
+ // cocoa rect.
+ DesktopIntRect rect;
+ rect.x = NSToIntRound(cocoaRect.origin.x);
+ rect.y = NSToIntRound(FlippedScreenY(cocoaRect.origin.y + cocoaRect.size.height));
+ rect.width = NSToIntRound(cocoaRect.origin.x + cocoaRect.size.width) - rect.x;
+ rect.height = NSToIntRound(FlippedScreenY(cocoaRect.origin.y)) - rect.y;
+ return rect;
+}
+
+LayoutDeviceIntRect nsCocoaUtils::CocoaRectToGeckoRectDevPix(const NSRect& aCocoaRect,
+ CGFloat aBackingScale) {
+ LayoutDeviceIntRect rect;
+ rect.x = NSToIntRound(aCocoaRect.origin.x * aBackingScale);
+ rect.y =
+ NSToIntRound(FlippedScreenY(aCocoaRect.origin.y + aCocoaRect.size.height) * aBackingScale);
+ rect.width = NSToIntRound((aCocoaRect.origin.x + aCocoaRect.size.width) * aBackingScale) - rect.x;
+ rect.height = NSToIntRound(FlippedScreenY(aCocoaRect.origin.y) * aBackingScale) - rect.y;
+ return rect;
+}
+
+NSPoint nsCocoaUtils::ScreenLocationForEvent(NSEvent* anEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Don't trust mouse locations of mouse move events, see bug 443178.
+ if (!anEvent || [anEvent type] == NSEventTypeMouseMoved) return [NSEvent mouseLocation];
+
+ // Pin momentum scroll events to the location of the last user-controlled
+ // scroll event.
+ if (IsMomentumScrollEvent(anEvent)) return ChildViewMouseTracker::sLastScrollEventScreenLocation;
+
+ return nsCocoaUtils::ConvertPointToScreen([anEvent window], [anEvent locationInWindow]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
+}
+
+BOOL nsCocoaUtils::IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return NSPointInRect(ScreenLocationForEvent(anEvent), [aWindow frame]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+NSPoint nsCocoaUtils::EventLocationForWindow(NSEvent* anEvent, NSWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return nsCocoaUtils::ConvertPointFromScreen(aWindow, ScreenLocationForEvent(anEvent));
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
+}
+
+BOOL nsCocoaUtils::IsMomentumScrollEvent(NSEvent* aEvent) {
+ return [aEvent type] == NSEventTypeScrollWheel && [aEvent momentumPhase] != NSEventPhaseNone;
+}
+
+BOOL nsCocoaUtils::EventHasPhaseInformation(NSEvent* aEvent) {
+ return [aEvent phase] != NSEventPhaseNone || [aEvent momentumPhase] != NSEventPhaseNone;
+}
+
+void nsCocoaUtils::HideOSChromeOnScreen(bool aShouldHide) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Keep track of how many hiding requests have been made, so that they can
+ // be nested.
+ static int sHiddenCount = 0;
+
+ sHiddenCount += aShouldHide ? 1 : -1;
+ NS_ASSERTION(sHiddenCount >= 0, "Unbalanced HideMenuAndDockForWindow calls");
+
+ NSApplicationPresentationOptions options =
+ sHiddenCount <= 0 ? NSApplicationPresentationDefault
+ : NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
+ [NSApp setPresentationOptions:options];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
+nsIWidget* nsCocoaUtils::GetHiddenWindowWidget() {
+ nsCOMPtr<nsIAppShellService> appShell(do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ if (!appShell) {
+ NS_WARNING("Couldn't get AppShellService in order to get hidden window ref");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAppWindow> hiddenWindow;
+ appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow));
+ if (!hiddenWindow) {
+ // Don't warn, this happens during shutdown, bug 358607.
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIBaseWindow> baseHiddenWindow;
+ baseHiddenWindow = do_GetInterface(hiddenWindow);
+ if (!baseHiddenWindow) {
+ NS_WARNING("Couldn't get nsIBaseWindow from hidden window (nsIAppWindow)");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> hiddenWindowWidget;
+ if (NS_FAILED(baseHiddenWindow->GetMainWidget(getter_AddRefs(hiddenWindowWidget)))) {
+ NS_WARNING("Couldn't get nsIWidget from hidden window (nsIBaseWindow)");
+ return nullptr;
+ }
+
+ return hiddenWindowWidget;
+}
+
+BOOL nsCocoaUtils::WasLaunchedAtLogin() {
+ ProcessSerialNumber processSerialNumber = {0, kCurrentProcess};
+ ProcessInfoRec processInfoRec = {};
+ processInfoRec.processInfoLength = sizeof(processInfoRec);
+
+ // There is currently no replacement for ::GetProcessInformation, which has
+ // been deprecated since macOS 10.9.
+ if (::GetProcessInformation(&processSerialNumber, &processInfoRec) == noErr) {
+ ProcessInfoRec parentProcessInfo = {};
+ parentProcessInfo.processInfoLength = sizeof(parentProcessInfo);
+ if (::GetProcessInformation(&processInfoRec.processLauncher, &parentProcessInfo) == noErr) {
+ return parentProcessInfo.processSignature == 'lgnw';
+ }
+ }
+ return NO;
+}
+
+BOOL nsCocoaUtils::ShouldRestoreStateDueToLaunchAtLoginImpl() {
+ // Check if we were launched by macOS as a result of having
+ // "Reopen windows..." selected during a restart.
+ if (!WasLaunchedAtLogin()) {
+ return NO;
+ }
+
+ CFStringRef lgnwPlistName = CFSTR("com.apple.loginwindow");
+ CFStringRef saveStateKey = CFSTR("TALLogoutSavesState");
+ CFPropertyListRef lgnwPlist =
+ (CFPropertyListRef)(::CFPreferencesCopyAppValue(saveStateKey, lgnwPlistName));
+ // The .plist doesn't exist unless the user changed the "Reopen windows..."
+ // preference. If it doesn't exist, restore by default (as this is the macOS
+ // default).
+ // https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CustomLogin.html
+ if (!lgnwPlist) {
+ return YES;
+ }
+
+ if (CFBooleanRef shouldRestoreState = static_cast<CFBooleanRef>(lgnwPlist)) {
+ return ::CFBooleanGetValue(shouldRestoreState);
+ }
+
+ return NO;
+}
+
+BOOL nsCocoaUtils::ShouldRestoreStateDueToLaunchAtLogin() {
+ BOOL shouldRestore = ShouldRestoreStateDueToLaunchAtLoginImpl();
+ Telemetry::ScalarSet(Telemetry::ScalarID::STARTUP_IS_RESTORED_BY_MACOS, !!shouldRestore);
+ mozilla::glean::startup::is_restored_by_macos.Set(!!shouldRestore);
+ return shouldRestore;
+}
+
+void nsCocoaUtils::PrepareForNativeAppModalDialog() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Don't do anything if this is embedding. We'll assume that if there is no hidden
+ // window we shouldn't do anything, and that should cover the embedding case.
+ nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (!hiddenWindowMenuBar) return;
+
+ // First put up the hidden window menu bar so that app menu event handling is correct.
+ hiddenWindowMenuBar->Paint();
+
+ NSMenu* mainMenu = [NSApp mainMenu];
+ NS_ASSERTION([mainMenu numberOfItems] > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ // Create new menu bar for use with modal dialog
+ NSMenu* newMenuBar = [[NSMenu alloc] initWithTitle:@""];
+
+ // Swap in our app menu. Note that the event target is whatever window is up when
+ // the app modal dialog goes up.
+ NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain];
+ [mainMenu removeItemAtIndex:0];
+ [newMenuBar insertItem:firstMenuItem atIndex:0];
+ [firstMenuItem release];
+
+ // Add standard edit menu
+ [newMenuBar addItem:nsMenuUtilsX::GetStandardEditMenuItem()];
+
+ // Show the new menu bar
+ [NSApp setMainMenu:newMenuBar];
+ [newMenuBar release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaUtils::CleanUpAfterNativeAppModalDialog() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Don't do anything if this is embedding. We'll assume that if there is no hidden
+ // window we shouldn't do anything, and that should cover the embedding case.
+ nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (!hiddenWindowMenuBar) return;
+
+ NSWindow* mainWindow = [NSApp mainWindow];
+ if (!mainWindow)
+ hiddenWindowMenuBar->Paint();
+ else
+ [WindowDelegate paintMenubarForWindow:mainWindow];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static void data_ss_release_callback(void* aDataSourceSurface, const void* data, size_t size) {
+ if (aDataSourceSurface) {
+ static_cast<DataSourceSurface*>(aDataSourceSurface)->Unmap();
+ static_cast<DataSourceSurface*>(aDataSourceSurface)->Release();
+ }
+}
+
+// This function assumes little endian byte order.
+static bool ComputeIsEntirelyBlack(const DataSourceSurface::MappedSurface& aMap,
+ const IntSize& aSize) {
+ for (int32_t y = 0; y < aSize.height; y++) {
+ size_t rowStart = y * aMap.mStride;
+ for (int32_t x = 0; x < aSize.width; x++) {
+ size_t index = rowStart + x * 4;
+ if (aMap.mData[index + 0] != 0 || aMap.mData[index + 1] != 0 || aMap.mData[index + 2] != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+nsresult nsCocoaUtils::CreateCGImageFromSurface(SourceSurface* aSurface, CGImageRef* aResult,
+ bool* aIsEntirelyBlack) {
+ RefPtr<DataSourceSurface> dataSurface;
+
+ if (aSurface->GetFormat() == SurfaceFormat::B8G8R8A8) {
+ dataSurface = aSurface->GetDataSurface();
+ } else {
+ // CGImageCreate only supports 16- and 32-bit bit-depth
+ // Convert format to SurfaceFormat::B8G8R8A8
+ dataSurface =
+ gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat(aSurface, SurfaceFormat::B8G8R8A8);
+ }
+
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+ if (height < 1 || width < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+ // The Unmap() call happens in data_ss_release_callback
+
+ if (aIsEntirelyBlack) {
+ *aIsEntirelyBlack = ComputeIsEntirelyBlack(map, dataSurface->GetSize());
+ }
+
+ // Create a CGImageRef with the bits from the image, taking into account
+ // the alpha ordering and endianness of the machine so we don't have to
+ // touch the bits ourselves.
+ CGDataProviderRef dataProvider = ::CGDataProviderCreateWithData(
+ dataSurface.forget().take(), map.mData, map.mStride * height, data_ss_release_callback);
+ CGColorSpaceRef colorSpace = ::CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
+ *aResult = ::CGImageCreate(width, height, 8, 32, map.mStride, colorSpace,
+ kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst,
+ dataProvider, NULL, 0, kCGRenderingIntentDefault);
+ ::CGColorSpaceRelease(colorSpace);
+ ::CGDataProviderRelease(dataProvider);
+ return *aResult ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsCocoaUtils::CreateNSImageFromCGImage(CGImageRef aInputImage, NSImage** aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Be very careful when creating the NSImage that the backing NSImageRep is
+ // exactly 1:1 with the input image. On a retina display, both [NSImage
+ // lockFocus] and [NSImage initWithCGImage:size:] will create an image with a
+ // 2x backing NSImageRep. This prevents NSCursor from recognizing a retina
+ // cursor, which only occurs if pixelsWide and pixelsHigh are exactly 2x the
+ // size of the NSImage.
+ //
+ // For example, if a 32x32 SVG cursor is rendered on a retina display, then
+ // aInputImage will be 64x64. The resulting NSImage will be scaled back down
+ // to 32x32 so it stays the correct size on the screen by changing its size
+ // (resizing a NSImage only scales the image and doesn't resample the data).
+ // If aInputImage is converted using [NSImage initWithCGImage:size:] then the
+ // bitmap will be 128x128 and NSCursor won't recognize a retina cursor, since
+ // it will expect a 64x64 bitmap.
+
+ int32_t width = ::CGImageGetWidth(aInputImage);
+ int32_t height = ::CGImageGetHeight(aInputImage);
+ NSRect imageRect = ::NSMakeRect(0.0, 0.0, width, height);
+
+ NSBitmapImageRep* offscreenRep =
+ [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+ pixelsWide:width
+ pixelsHigh:height
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bitmapFormat:NSAlphaFirstBitmapFormat
+ bytesPerRow:0
+ bitsPerPixel:0];
+
+ NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
+ [NSGraphicsContext saveGraphicsState];
+ [NSGraphicsContext setCurrentContext:context];
+
+ // Get the Quartz context and draw.
+ CGContextRef imageContext = [[NSGraphicsContext currentContext] CGContext];
+ ::CGContextDrawImage(imageContext, *(CGRect*)&imageRect, aInputImage);
+
+ [NSGraphicsContext restoreGraphicsState];
+
+ *aResult = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
+ [*aResult addRepresentation:offscreenRep];
+ [offscreenRep release];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaUtils::CreateNSImageFromImageContainer(imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext,
+ const ComputedStyle* aComputedStyle,
+ NSImage** aResult, CGFloat scaleFactor,
+ bool* aIsEntirelyBlack) {
+ RefPtr<SourceSurface> surface;
+ int32_t width = 0, height = 0;
+ aImage->GetWidth(&width);
+ aImage->GetHeight(&height);
+
+ // Render a vector image at the correct resolution on a retina display
+ if (aImage->GetType() == imgIContainer::TYPE_VECTOR) {
+ IntSize scaledSize = IntSize::Ceil(width * scaleFactor, height * scaleFactor);
+
+ RefPtr<DrawTarget> drawTarget = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ scaledSize, SurfaceFormat::B8G8R8A8);
+ if (!drawTarget || !drawTarget->IsValid()) {
+ NS_ERROR("Failed to create valid DrawTarget");
+ return NS_ERROR_FAILURE;
+ }
+
+ gfxContext context(drawTarget);
+
+ SVGImageContext svgContext;
+ if (aPresContext && aComputedStyle) {
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, *aComputedStyle, aImage);
+ }
+ mozilla::image::ImgDrawResult res =
+ aImage->Draw(&context, scaledSize, ImageRegion::Create(scaledSize), aWhichFrame,
+ SamplingFilter::POINT, svgContext, imgIContainer::FLAG_SYNC_DECODE, 1.0);
+
+ if (res != mozilla::image::ImgDrawResult::SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ surface = drawTarget->Snapshot();
+ } else {
+ surface = aImage->GetFrame(aWhichFrame,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ }
+
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ CGImageRef imageRef = NULL;
+ nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef, aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !imageRef) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = nsCocoaUtils::CreateNSImageFromCGImage(imageRef, aResult);
+ if (NS_FAILED(rv) || !aResult) {
+ return NS_ERROR_FAILURE;
+ }
+ ::CGImageRelease(imageRef);
+
+ // Ensure the image will be rendered the correct size on a retina display
+ NSSize size = NSMakeSize(width, height);
+ [*aResult setSize:size];
+ [[[*aResult representations] objectAtIndex:0] setSize:size];
+ return NS_OK;
+}
+
+nsresult nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame, const nsPresContext* aPresContext,
+ const ComputedStyle* aComputedStyle, NSImage** aResult, bool* aIsEntirelyBlack) {
+ int32_t width = 0, height = 0;
+ aImage->GetWidth(&width);
+ aImage->GetHeight(&height);
+ NSSize size = NSMakeSize(width, height);
+ *aResult = [[NSImage alloc] init];
+ [*aResult setSize:size];
+
+ NSImage* newRepresentation = nil;
+ nsresult rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aPresContext, aComputedStyle,
+ &newRepresentation, 1.0f, aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !newRepresentation) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [[[newRepresentation representations] objectAtIndex:0] setSize:size];
+ [*aResult addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
+ [newRepresentation release];
+ newRepresentation = nil;
+
+ rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aPresContext, aComputedStyle,
+ &newRepresentation, 2.0f, aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !newRepresentation) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [[[newRepresentation representations] objectAtIndex:0] setSize:size];
+ [*aResult addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
+ [newRepresentation release];
+ return NS_OK;
+}
+
+// static
+void nsCocoaUtils::GetStringForNSString(const NSString* aSrc, nsAString& aDist) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aSrc) {
+ aDist.Truncate();
+ return;
+ }
+
+ aDist.SetLength([aSrc length]);
+ [aSrc getCharacters:reinterpret_cast<unichar*>(aDist.BeginWriting())
+ range:NSMakeRange(0, [aSrc length])];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// static
+NSString* nsCocoaUtils::ToNSString(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aString.BeginReading())
+ length:aString.Length()];
+}
+
+// static
+NSString* nsCocoaUtils::ToNSString(const nsACString& aCString) {
+ if (aCString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [[[NSString alloc] initWithBytes:aCString.BeginReading()
+ length:aCString.Length()
+ encoding:NSUTF8StringEncoding] autorelease];
+}
+
+// static
+NSURL* nsCocoaUtils::ToNSURL(const nsAString& aURLString) {
+ nsAutoCString encodedURLString;
+ nsresult rv = NS_GetSpecWithNSURLEncoding(encodedURLString, NS_ConvertUTF16toUTF8(aURLString));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ NSString* encodedURLNSString = ToNSString(encodedURLString);
+ if (!encodedURLNSString) {
+ return nullptr;
+ }
+
+ return [NSURL URLWithString:encodedURLNSString];
+}
+
+// static
+void nsCocoaUtils::GeckoRectToNSRect(const nsIntRect& aGeckoRect, NSRect& aOutCocoaRect) {
+ aOutCocoaRect.origin.x = aGeckoRect.x;
+ aOutCocoaRect.origin.y = aGeckoRect.y;
+ aOutCocoaRect.size.width = aGeckoRect.width;
+ aOutCocoaRect.size.height = aGeckoRect.height;
+}
+
+// static
+void nsCocoaUtils::NSRectToGeckoRect(const NSRect& aCocoaRect, nsIntRect& aOutGeckoRect) {
+ aOutGeckoRect.x = NSToIntRound(aCocoaRect.origin.x);
+ aOutGeckoRect.y = NSToIntRound(aCocoaRect.origin.y);
+ aOutGeckoRect.width = NSToIntRound(aCocoaRect.origin.x + aCocoaRect.size.width) - aOutGeckoRect.x;
+ aOutGeckoRect.height =
+ NSToIntRound(aCocoaRect.origin.y + aCocoaRect.size.height) - aOutGeckoRect.y;
+}
+
+// static
+NSEvent* nsCocoaUtils::MakeNewCocoaEventWithType(NSEventType aEventType, NSEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSEvent* newEvent = [NSEvent keyEventWithType:aEventType
+ location:[aEvent locationInWindow]
+ modifierFlags:[aEvent modifierFlags]
+ timestamp:[aEvent timestamp]
+ windowNumber:[aEvent windowNumber]
+ context:nil
+ characters:[aEvent characters]
+ charactersIgnoringModifiers:[aEvent charactersIgnoringModifiers]
+ isARepeat:[aEvent isARepeat]
+ keyCode:[aEvent keyCode]];
+ return newEvent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// static
+NSEvent* nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(const WidgetKeyboardEvent& aKeyEvent,
+ NSInteger aWindowNumber,
+ NSGraphicsContext* aContext) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSEventType eventType;
+ if (aKeyEvent.mMessage == eKeyUp) {
+ eventType = NSEventTypeKeyUp;
+ } else {
+ eventType = NSEventTypeKeyDown;
+ }
+
+ static const uint32_t sModifierFlagMap[][2] = {{MODIFIER_SHIFT, NSEventModifierFlagShift},
+ {MODIFIER_CONTROL, NSEventModifierFlagControl},
+ {MODIFIER_ALT, NSEventModifierFlagOption},
+ {MODIFIER_ALTGRAPH, NSEventModifierFlagOption},
+ {MODIFIER_META, NSEventModifierFlagCommand},
+ {MODIFIER_CAPSLOCK, NSEventModifierFlagCapsLock},
+ {MODIFIER_NUMLOCK, NSEventModifierFlagNumericPad}};
+
+ NSUInteger modifierFlags = 0;
+ for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) {
+ if (aKeyEvent.mModifiers & sModifierFlagMap[i][0]) {
+ modifierFlags |= sModifierFlagMap[i][1];
+ }
+ }
+
+ NSString* characters;
+ if (aKeyEvent.mCharCode) {
+ characters =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(&(aKeyEvent.mCharCode))
+ length:1];
+ } else {
+ uint32_t cocoaCharCode = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aKeyEvent.mKeyCode);
+ characters = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(&cocoaCharCode)
+ length:1];
+ }
+
+ return [NSEvent keyEventWithType:eventType
+ location:NSMakePoint(0, 0)
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:aWindowNumber
+ context:aContext
+ characters:characters
+ charactersIgnoringModifiers:characters
+ isARepeat:NO
+ keyCode:0]; // Native key code not currently needed
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// static
+void nsCocoaUtils::InitInputEvent(WidgetInputEvent& aInputEvent, NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ aInputEvent.mModifiers = ModifiersForEvent(aNativeEvent);
+ aInputEvent.mTimeStamp = GetEventTimeStamp([aNativeEvent timestamp]);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// static
+Modifiers nsCocoaUtils::ModifiersForEvent(NSEvent* aNativeEvent) {
+ NSUInteger modifiers = aNativeEvent ? [aNativeEvent modifierFlags] : [NSEvent modifierFlags];
+ Modifiers result = 0;
+ if (modifiers & NSEventModifierFlagShift) {
+ result |= MODIFIER_SHIFT;
+ }
+ if (modifiers & NSEventModifierFlagControl) {
+ result |= MODIFIER_CONTROL;
+ }
+ if (modifiers & NSEventModifierFlagOption) {
+ result |= MODIFIER_ALT;
+ // Mac's option key is similar to other platforms' AltGr key.
+ // Let's set AltGr flag when option key is pressed for consistency with
+ // other platforms.
+ result |= MODIFIER_ALTGRAPH;
+ }
+ if (modifiers & NSEventModifierFlagCommand) {
+ result |= MODIFIER_META;
+ }
+
+ if (modifiers & NSEventModifierFlagCapsLock) {
+ result |= MODIFIER_CAPSLOCK;
+ }
+ // Mac doesn't have NumLock key. We can assume that NumLock is always locked
+ // if user is using a keyboard which has numpad. Otherwise, if user is using
+ // a keyboard which doesn't have numpad, e.g., MacBook's keyboard, we can
+ // assume that NumLock is always unlocked.
+ // Unfortunately, we cannot know whether current keyboard has numpad or not.
+ // We should notify locked state only when keys in numpad are pressed.
+ // By this, web applications may not be confused by unexpected numpad key's
+ // key event with unlocked state.
+ if (modifiers & NSEventModifierFlagNumericPad) {
+ result |= MODIFIER_NUMLOCK;
+ }
+
+ // Be aware, NSEventModifierFlagFunction is included when arrow keys, home key or some
+ // other keys are pressed. We cannot check whether 'fn' key is pressed or
+ // not by the flag.
+
+ return result;
+}
+
+// static
+UInt32 nsCocoaUtils::ConvertToCarbonModifier(NSUInteger aCocoaModifier) {
+ UInt32 carbonModifier = 0;
+ if (aCocoaModifier & NSEventModifierFlagCapsLock) {
+ carbonModifier |= alphaLock;
+ }
+ if (aCocoaModifier & NSEventModifierFlagControl) {
+ carbonModifier |= controlKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagOption) {
+ carbonModifier |= optionKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagShift) {
+ carbonModifier |= shiftKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagCommand) {
+ carbonModifier |= cmdKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagNumericPad) {
+ carbonModifier |= kEventKeyModifierNumLockMask;
+ }
+ if (aCocoaModifier & NSEventModifierFlagFunction) {
+ carbonModifier |= kEventKeyModifierFnMask;
+ }
+ return carbonModifier;
+}
+
+// While HiDPI support is not 100% complete and tested, we'll have a pref
+// to allow it to be turned off in case of problems (or for testing purposes).
+
+// gfx.hidpi.enabled is an integer with the meaning:
+// <= 0 : HiDPI support is disabled
+// 1 : HiDPI enabled provided all screens have the same backing resolution
+// > 1 : HiDPI enabled even if there are a mixture of screen modes
+
+// All the following code is to be removed once HiDPI work is more complete.
+
+static bool sHiDPIEnabled = false;
+static bool sHiDPIPrefInitialized = false;
+
+// static
+bool nsCocoaUtils::HiDPIEnabled() {
+ if (!sHiDPIPrefInitialized) {
+ sHiDPIPrefInitialized = true;
+
+ int prefSetting = Preferences::GetInt("gfx.hidpi.enabled", 1);
+ if (prefSetting <= 0) {
+ return false;
+ }
+
+ // prefSetting is at least 1, need to check attached screens...
+
+ int scaleFactors = 0; // used as a bitset to track the screen types found
+ NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
+ while (NSScreen* screen = [screenEnum nextObject]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ // Currently, we only care about differentiating "1.0" and "2.0",
+ // so we set one of the two low bits to record which.
+ if ([screen backingScaleFactor] > 1.0) {
+ scaleFactors |= 2;
+ } else {
+ scaleFactors |= 1;
+ }
+ }
+
+ // Now scaleFactors will be:
+ // 0 if no screens (supporting backingScaleFactor) found
+ // 1 if only lo-DPI screens
+ // 2 if only hi-DPI screens
+ // 3 if both lo- and hi-DPI screens
+ // We'll enable HiDPI support if there's only a single screen type,
+ // OR if the pref setting is explicitly greater than 1.
+ sHiDPIEnabled = (scaleFactors <= 2) || (prefSetting > 1);
+ }
+
+ return sHiDPIEnabled;
+}
+
+// static
+void nsCocoaUtils::InvalidateHiDPIState() { sHiDPIPrefInitialized = false; }
+
+void nsCocoaUtils::GetCommandsFromKeyEvent(NSEvent* aEvent,
+ nsTArray<KeyBindingsCommand>& aCommands) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aEvent);
+
+ static NativeKeyBindingsRecorder* sNativeKeyBindingsRecorder;
+ if (!sNativeKeyBindingsRecorder) {
+ sNativeKeyBindingsRecorder = [NativeKeyBindingsRecorder new];
+ }
+
+ [sNativeKeyBindingsRecorder startRecording:aCommands];
+
+ // This will trigger 0 - N calls to doCommandBySelector: and insertText:
+ [sNativeKeyBindingsRecorder interpretKeyEvents:[NSArray arrayWithObject:aEvent]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@implementation NativeKeyBindingsRecorder
+
+- (void)startRecording:(nsTArray<KeyBindingsCommand>&)aCommands {
+ mCommands = &aCommands;
+ mCommands->Clear();
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ KeyBindingsCommand command = {aSelector, nil};
+
+ mCommands->AppendElement(command);
+}
+
+- (void)insertText:(id)aString {
+ KeyBindingsCommand command = {@selector(insertText:), aString};
+
+ mCommands->AppendElement(command);
+}
+
+@end // NativeKeyBindingsRecorder
+
+struct KeyConversionData {
+ const char* str;
+ size_t strLength;
+ uint32_t geckoKeyCode;
+ uint32_t charCode;
+};
+
+static const KeyConversionData gKeyConversions[] = {
+
+#define KEYCODE_ENTRY(aStr, aCode) \
+ { \
+# aStr, sizeof(#aStr) - 1, NS_##aStr, aCode \
+ }
+
+// Some keycodes may have different name in KeyboardEvent from its key name.
+#define KEYCODE_ENTRY2(aStr, aNSName, aCode) \
+ { \
+# aStr, sizeof(#aStr) - 1, NS_##aNSName, aCode \
+ }
+
+ KEYCODE_ENTRY(VK_CANCEL, 0x001B),
+ KEYCODE_ENTRY(VK_DELETE, NSDeleteFunctionKey),
+ KEYCODE_ENTRY(VK_BACK, NSBackspaceCharacter),
+ KEYCODE_ENTRY2(VK_BACK_SPACE, VK_BACK, NSBackspaceCharacter),
+ KEYCODE_ENTRY(VK_TAB, NSTabCharacter),
+ KEYCODE_ENTRY(VK_CLEAR, NSClearLineFunctionKey),
+ KEYCODE_ENTRY(VK_RETURN, NSEnterCharacter),
+ KEYCODE_ENTRY(VK_SHIFT, 0),
+ KEYCODE_ENTRY(VK_CONTROL, 0),
+ KEYCODE_ENTRY(VK_ALT, 0),
+ KEYCODE_ENTRY(VK_PAUSE, NSPauseFunctionKey),
+ KEYCODE_ENTRY(VK_CAPS_LOCK, 0),
+ KEYCODE_ENTRY(VK_ESCAPE, 0),
+ KEYCODE_ENTRY(VK_SPACE, ' '),
+ KEYCODE_ENTRY(VK_PAGE_UP, NSPageUpFunctionKey),
+ KEYCODE_ENTRY(VK_PAGE_DOWN, NSPageDownFunctionKey),
+ KEYCODE_ENTRY(VK_END, NSEndFunctionKey),
+ KEYCODE_ENTRY(VK_HOME, NSHomeFunctionKey),
+ KEYCODE_ENTRY(VK_LEFT, NSLeftArrowFunctionKey),
+ KEYCODE_ENTRY(VK_UP, NSUpArrowFunctionKey),
+ KEYCODE_ENTRY(VK_RIGHT, NSRightArrowFunctionKey),
+ KEYCODE_ENTRY(VK_DOWN, NSDownArrowFunctionKey),
+ KEYCODE_ENTRY(VK_PRINTSCREEN, NSPrintScreenFunctionKey),
+ KEYCODE_ENTRY(VK_INSERT, NSInsertFunctionKey),
+ KEYCODE_ENTRY(VK_HELP, NSHelpFunctionKey),
+ KEYCODE_ENTRY(VK_0, '0'),
+ KEYCODE_ENTRY(VK_1, '1'),
+ KEYCODE_ENTRY(VK_2, '2'),
+ KEYCODE_ENTRY(VK_3, '3'),
+ KEYCODE_ENTRY(VK_4, '4'),
+ KEYCODE_ENTRY(VK_5, '5'),
+ KEYCODE_ENTRY(VK_6, '6'),
+ KEYCODE_ENTRY(VK_7, '7'),
+ KEYCODE_ENTRY(VK_8, '8'),
+ KEYCODE_ENTRY(VK_9, '9'),
+ KEYCODE_ENTRY(VK_SEMICOLON, ':'),
+ KEYCODE_ENTRY(VK_EQUALS, '='),
+ KEYCODE_ENTRY(VK_A, 'A'),
+ KEYCODE_ENTRY(VK_B, 'B'),
+ KEYCODE_ENTRY(VK_C, 'C'),
+ KEYCODE_ENTRY(VK_D, 'D'),
+ KEYCODE_ENTRY(VK_E, 'E'),
+ KEYCODE_ENTRY(VK_F, 'F'),
+ KEYCODE_ENTRY(VK_G, 'G'),
+ KEYCODE_ENTRY(VK_H, 'H'),
+ KEYCODE_ENTRY(VK_I, 'I'),
+ KEYCODE_ENTRY(VK_J, 'J'),
+ KEYCODE_ENTRY(VK_K, 'K'),
+ KEYCODE_ENTRY(VK_L, 'L'),
+ KEYCODE_ENTRY(VK_M, 'M'),
+ KEYCODE_ENTRY(VK_N, 'N'),
+ KEYCODE_ENTRY(VK_O, 'O'),
+ KEYCODE_ENTRY(VK_P, 'P'),
+ KEYCODE_ENTRY(VK_Q, 'Q'),
+ KEYCODE_ENTRY(VK_R, 'R'),
+ KEYCODE_ENTRY(VK_S, 'S'),
+ KEYCODE_ENTRY(VK_T, 'T'),
+ KEYCODE_ENTRY(VK_U, 'U'),
+ KEYCODE_ENTRY(VK_V, 'V'),
+ KEYCODE_ENTRY(VK_W, 'W'),
+ KEYCODE_ENTRY(VK_X, 'X'),
+ KEYCODE_ENTRY(VK_Y, 'Y'),
+ KEYCODE_ENTRY(VK_Z, 'Z'),
+ KEYCODE_ENTRY(VK_CONTEXT_MENU, NSMenuFunctionKey),
+ KEYCODE_ENTRY(VK_NUMPAD0, '0'),
+ KEYCODE_ENTRY(VK_NUMPAD1, '1'),
+ KEYCODE_ENTRY(VK_NUMPAD2, '2'),
+ KEYCODE_ENTRY(VK_NUMPAD3, '3'),
+ KEYCODE_ENTRY(VK_NUMPAD4, '4'),
+ KEYCODE_ENTRY(VK_NUMPAD5, '5'),
+ KEYCODE_ENTRY(VK_NUMPAD6, '6'),
+ KEYCODE_ENTRY(VK_NUMPAD7, '7'),
+ KEYCODE_ENTRY(VK_NUMPAD8, '8'),
+ KEYCODE_ENTRY(VK_NUMPAD9, '9'),
+ KEYCODE_ENTRY(VK_MULTIPLY, '*'),
+ KEYCODE_ENTRY(VK_ADD, '+'),
+ KEYCODE_ENTRY(VK_SEPARATOR, 0),
+ KEYCODE_ENTRY(VK_SUBTRACT, '-'),
+ KEYCODE_ENTRY(VK_DECIMAL, '.'),
+ KEYCODE_ENTRY(VK_DIVIDE, '/'),
+ KEYCODE_ENTRY(VK_F1, NSF1FunctionKey),
+ KEYCODE_ENTRY(VK_F2, NSF2FunctionKey),
+ KEYCODE_ENTRY(VK_F3, NSF3FunctionKey),
+ KEYCODE_ENTRY(VK_F4, NSF4FunctionKey),
+ KEYCODE_ENTRY(VK_F5, NSF5FunctionKey),
+ KEYCODE_ENTRY(VK_F6, NSF6FunctionKey),
+ KEYCODE_ENTRY(VK_F7, NSF7FunctionKey),
+ KEYCODE_ENTRY(VK_F8, NSF8FunctionKey),
+ KEYCODE_ENTRY(VK_F9, NSF9FunctionKey),
+ KEYCODE_ENTRY(VK_F10, NSF10FunctionKey),
+ KEYCODE_ENTRY(VK_F11, NSF11FunctionKey),
+ KEYCODE_ENTRY(VK_F12, NSF12FunctionKey),
+ KEYCODE_ENTRY(VK_F13, NSF13FunctionKey),
+ KEYCODE_ENTRY(VK_F14, NSF14FunctionKey),
+ KEYCODE_ENTRY(VK_F15, NSF15FunctionKey),
+ KEYCODE_ENTRY(VK_F16, NSF16FunctionKey),
+ KEYCODE_ENTRY(VK_F17, NSF17FunctionKey),
+ KEYCODE_ENTRY(VK_F18, NSF18FunctionKey),
+ KEYCODE_ENTRY(VK_F19, NSF19FunctionKey),
+ KEYCODE_ENTRY(VK_F20, NSF20FunctionKey),
+ KEYCODE_ENTRY(VK_F21, NSF21FunctionKey),
+ KEYCODE_ENTRY(VK_F22, NSF22FunctionKey),
+ KEYCODE_ENTRY(VK_F23, NSF23FunctionKey),
+ KEYCODE_ENTRY(VK_F24, NSF24FunctionKey),
+ KEYCODE_ENTRY(VK_NUM_LOCK, NSClearLineFunctionKey),
+ KEYCODE_ENTRY(VK_SCROLL_LOCK, NSScrollLockFunctionKey),
+ KEYCODE_ENTRY(VK_COMMA, ','),
+ KEYCODE_ENTRY(VK_PERIOD, '.'),
+ KEYCODE_ENTRY(VK_SLASH, '/'),
+ KEYCODE_ENTRY(VK_BACK_QUOTE, '`'),
+ KEYCODE_ENTRY(VK_OPEN_BRACKET, '['),
+ KEYCODE_ENTRY(VK_BACK_SLASH, '\\'),
+ KEYCODE_ENTRY(VK_CLOSE_BRACKET, ']'),
+ KEYCODE_ENTRY(VK_QUOTE, '\'')
+
+#undef KEYCODE_ENTRY
+
+};
+
+uint32_t nsCocoaUtils::ConvertGeckoNameToMacCharCode(const nsAString& aKeyCodeName) {
+ if (aKeyCodeName.IsEmpty()) {
+ return 0;
+ }
+
+ nsAutoCString keyCodeName;
+ LossyCopyUTF16toASCII(aKeyCodeName, keyCodeName);
+ // We want case-insensitive comparison with data stored as uppercase.
+ ToUpperCase(keyCodeName);
+
+ uint32_t keyCodeNameLength = keyCodeName.Length();
+ const char* keyCodeNameStr = keyCodeName.get();
+ for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
+ if (keyCodeNameLength == gKeyConversions[i].strLength &&
+ nsCRT::strcmp(gKeyConversions[i].str, keyCodeNameStr) == 0) {
+ return gKeyConversions[i].charCode;
+ }
+ }
+
+ return 0;
+}
+
+uint32_t nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(uint32_t aKeyCode) {
+ if (!aKeyCode) {
+ return 0;
+ }
+
+ for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
+ if (gKeyConversions[i].geckoKeyCode == aKeyCode) {
+ return gKeyConversions[i].charCode;
+ }
+ }
+
+ return 0;
+}
+
+NSEventModifierFlags nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
+ nsIWidget::Modifiers aNativeModifiers) {
+ if (!aNativeModifiers) {
+ return 0;
+ }
+ struct ModifierFlagMapEntry {
+ nsIWidget::Modifiers mWidgetModifier;
+ NSEventModifierFlags mModifierFlags;
+ };
+ static constexpr ModifierFlagMapEntry sModifierFlagMap[] = {
+ {nsIWidget::CAPS_LOCK, NSEventModifierFlagCapsLock},
+ {nsIWidget::SHIFT_L, NSEventModifierFlagShift | 0x0002},
+ {nsIWidget::SHIFT_R, NSEventModifierFlagShift | 0x0004},
+ {nsIWidget::CTRL_L, NSEventModifierFlagControl | 0x0001},
+ {nsIWidget::CTRL_R, NSEventModifierFlagControl | 0x2000},
+ {nsIWidget::ALT_L, NSEventModifierFlagOption | 0x0020},
+ {nsIWidget::ALT_R, NSEventModifierFlagOption | 0x0040},
+ {nsIWidget::COMMAND_L, NSEventModifierFlagCommand | 0x0008},
+ {nsIWidget::COMMAND_R, NSEventModifierFlagCommand | 0x0010},
+ {nsIWidget::NUMERIC_KEY_PAD, NSEventModifierFlagNumericPad},
+ {nsIWidget::HELP, NSEventModifierFlagHelp},
+ {nsIWidget::FUNCTION, NSEventModifierFlagFunction}};
+
+ NSEventModifierFlags modifierFlags = 0;
+ for (const ModifierFlagMapEntry& entry : sModifierFlagMap) {
+ if (aNativeModifiers & entry.mWidgetModifier) {
+ modifierFlags |= entry.mModifierFlags;
+ }
+ }
+ return modifierFlags;
+}
+
+mozilla::MouseButton nsCocoaUtils::ButtonForEvent(NSEvent* aEvent) {
+ switch (aEvent.type) {
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeLeftMouseUp:
+ return MouseButton::ePrimary;
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeRightMouseUp:
+ return MouseButton::eSecondary;
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseDragged:
+ case NSEventTypeOtherMouseUp:
+ switch (aEvent.buttonNumber) {
+ case 3:
+ return MouseButton::eX1;
+ case 4:
+ return MouseButton::eX2;
+ default:
+ // The middle button usually has button 2, but if this is a synthesized event (for which
+ // you cannot specify a buttonNumber), then the button will be 0. Treat all remaining
+ // OtherMouse events as the middle button.
+ return MouseButton::eMiddle;
+ }
+ default:
+ // Treat non-mouse events as the primary mouse button.
+ return MouseButton::ePrimary;
+ }
+}
+
+NSMutableAttributedString* nsCocoaUtils::GetNSMutableAttributedString(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRanges, const bool aIsVertical,
+ const CGFloat aBackingScaleFactor) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ NSString* nsstr = nsCocoaUtils::ToNSString(aText);
+ NSMutableAttributedString* attrStr =
+ [[[NSMutableAttributedString alloc] initWithString:nsstr attributes:nil] autorelease];
+
+ int32_t lastOffset = aText.Length();
+ for (auto i = aFontRanges.Length(); i > 0; --i) {
+ const FontRange& fontRange = aFontRanges[i - 1];
+ NSString* fontName = nsCocoaUtils::ToNSString(fontRange.mFontName);
+ CGFloat fontSize = fontRange.mFontSize / aBackingScaleFactor;
+ NSFont* font = [NSFont fontWithName:fontName size:fontSize];
+ if (!font) {
+ font = [NSFont systemFontOfSize:fontSize];
+ }
+
+ NSDictionary* attrs = @{NSFontAttributeName : font};
+ NSRange range = NSMakeRange(fontRange.mStartOffset, lastOffset - fontRange.mStartOffset);
+ [attrStr setAttributes:attrs range:range];
+ lastOffset = fontRange.mStartOffset;
+ }
+
+ if (aIsVertical) {
+ [attrStr addAttribute:NSVerticalGlyphFormAttributeName
+ value:[NSNumber numberWithInt:1]
+ range:NSMakeRange(0, [attrStr length])];
+ }
+
+ return attrStr;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil)
+}
+
+TimeStamp nsCocoaUtils::GetEventTimeStamp(NSTimeInterval aEventTime) {
+ if (!aEventTime) {
+ // If the event is generated by a 3rd party application, its timestamp
+ // may be 0. In this case, just return current timestamp.
+ // XXX Should we cache last event time?
+ return TimeStamp::Now();
+ }
+ // The internal value of the macOS implementation of TimeStamp is based on
+ // mach_absolute_time(), which measures "ticks" since boot.
+ // Event timestamps are NSTimeIntervals (seconds) since boot. So the two time
+ // representations already have the same base; we only need to convert
+ // seconds into ticks.
+ int64_t tick = BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aEventTime * 1000.0);
+ return TimeStamp::FromSystemTime(tick);
+}
+
+static NSString* ActionOnDoubleClickSystemPref() {
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ NSString* kAppleActionOnDoubleClickKey = @"AppleActionOnDoubleClick";
+ id value = [userDefaults objectForKey:kAppleActionOnDoubleClickKey];
+ if ([value isKindOfClass:[NSString class]]) {
+ return value;
+ }
+ return nil;
+}
+
+@interface NSWindow (NSWindowShouldZoomOnDoubleClick)
++ (BOOL)_shouldZoomOnDoubleClick; // present on 10.7 and above
+@end
+
+bool nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick() {
+ if ([NSWindow respondsToSelector:@selector(_shouldZoomOnDoubleClick)]) {
+ return [NSWindow _shouldZoomOnDoubleClick];
+ }
+ return [ActionOnDoubleClickSystemPref() isEqualToString:@"Maximize"];
+}
+
+bool nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick() {
+ // Check the system preferences.
+ // We could also check -[NSWindow _shouldMiniaturizeOnDoubleClick]. It's not clear to me which
+ // approach would be preferable; neither is public API.
+ return [ActionOnDoubleClickSystemPref() isEqualToString:@"Minimize"];
+}
+
+// AVAuthorizationStatus is not needed unless we are running on 10.14.
+// However, on pre-10.14 SDK's, AVAuthorizationStatus and its enum values
+// are both defined and prohibited from use by compile-time checks. We
+// define a copy of AVAuthorizationStatus to allow compilation on pre-10.14
+// SDK's. The enum values must match what is defined in the 10.14 SDK.
+// We use ASSERTS for 10.14 SDK builds to check the enum values match.
+enum GeckoAVAuthorizationStatus : NSInteger {
+ GeckoAVAuthorizationStatusNotDetermined = 0,
+ GeckoAVAuthorizationStatusRestricted = 1,
+ GeckoAVAuthorizationStatusDenied = 2,
+ GeckoAVAuthorizationStatusAuthorized = 3
+};
+
+#if !defined(MAC_OS_X_VERSION_10_14) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_14
+// Define authorizationStatusForMediaType: as returning
+// GeckoAVAuthorizationStatus instead of AVAuthorizationStatus to allow
+// compilation on pre-10.14 SDK's.
+@interface AVCaptureDevice (GeckoAVAuthorizationStatus)
++ (GeckoAVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType;
+@end
+
+@interface AVCaptureDevice (WithCompletionHandler)
++ (void)requestAccessForMediaType:(AVMediaType)mediaType
+ completionHandler:(void (^)(BOOL granted))handler;
+@end
+#endif
+
+static const char* AVMediaTypeToString(AVMediaType aType) {
+ if (aType == AVMediaTypeVideo) {
+ return "video";
+ }
+
+ if (aType == AVMediaTypeAudio) {
+ return "audio";
+ }
+
+ return "unexpected type";
+}
+
+static void LogAuthorizationStatus(AVMediaType aType, int aState) {
+ const char* stateString;
+
+ switch (aState) {
+ case GeckoAVAuthorizationStatusAuthorized:
+ stateString = "AVAuthorizationStatusAuthorized";
+ break;
+ case GeckoAVAuthorizationStatusDenied:
+ stateString = "AVAuthorizationStatusDenied";
+ break;
+ case GeckoAVAuthorizationStatusNotDetermined:
+ stateString = "AVAuthorizationStatusNotDetermined";
+ break;
+ case GeckoAVAuthorizationStatusRestricted:
+ stateString = "AVAuthorizationStatusRestricted";
+ break;
+ default:
+ stateString = "Invalid state";
+ }
+
+ LOG("%s authorization status: %s\n", AVMediaTypeToString(aType), stateString);
+}
+
+static nsresult GetPermissionState(AVMediaType aMediaType, uint16_t& aState) {
+ MOZ_ASSERT(aMediaType == AVMediaTypeVideo || aMediaType == AVMediaTypeAudio);
+
+ // Only attempt to check authorization status on 10.14+.
+ if (@available(macOS 10.14, *)) {
+ GeckoAVAuthorizationStatus authStatus = static_cast<GeckoAVAuthorizationStatus>(
+ [AVCaptureDevice authorizationStatusForMediaType:aMediaType]);
+ LogAuthorizationStatus(aMediaType, authStatus);
+
+ // Convert GeckoAVAuthorizationStatus to nsIOSPermissionRequest const
+ switch (authStatus) {
+ case GeckoAVAuthorizationStatusAuthorized:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ return NS_OK;
+ case GeckoAVAuthorizationStatusDenied:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
+ return NS_OK;
+ case GeckoAVAuthorizationStatusNotDetermined:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
+ return NS_OK;
+ case GeckoAVAuthorizationStatusRestricted:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_RESTRICTED;
+ return NS_OK;
+ default:
+ MOZ_ASSERT(false, "Invalid authorization status");
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsCocoaUtils::GetVideoCapturePermissionState(uint16_t& aPermissionState) {
+ return GetPermissionState(AVMediaTypeVideo, aPermissionState);
+}
+
+nsresult nsCocoaUtils::GetAudioCapturePermissionState(uint16_t& aPermissionState) {
+ return GetPermissionState(AVMediaTypeAudio, aPermissionState);
+}
+
+// Set |aPermissionState| to PERMISSION_STATE_AUTHORIZED if this application
+// has already been granted permission to record the screen in macOS Security
+// and Privacy system settings. If we do not have permission (because the user
+// hasn't yet been asked yet or the user previously denied the prompt), use
+// PERMISSION_STATE_DENIED. Returns NS_ERROR_NOT_IMPLEMENTED on macOS 10.14
+// and earlier.
+nsresult nsCocoaUtils::GetScreenCapturePermissionState(uint16_t& aPermissionState) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
+
+ // Only attempt to check screen recording authorization status on 10.15+.
+ // On earlier macOS versions, screen recording is allowed by default.
+ if (@available(macOS 10.15, *)) {
+ if (!StaticPrefs::media_macos_screenrecording_oscheck_enabled()) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ LOG("screen authorization status: authorized (test disabled via pref)");
+ return NS_OK;
+ }
+
+ // Unlike with camera and microphone capture, there is no support for
+ // checking the screen recording permission status. Instead, an application
+ // can use the presence of window names (which are privacy sensitive) in
+ // the window info list as an indication. The list only includes window
+ // names if the calling application has been authorized to record the
+ // screen. We use the window name, window level, and owning PID as
+ // heuristics to determine if we have screen recording permission.
+ AutoCFRelease<CFArrayRef> windowArray =
+ CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
+ if (!windowArray) {
+ LOG("GetScreenCapturePermissionState() ERROR: got NULL window info list");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ int32_t windowLevelDock = CGWindowLevelForKey(kCGDockWindowLevelKey);
+ int32_t windowLevelNormal = CGWindowLevelForKey(kCGNormalWindowLevelKey);
+ LOG("GetScreenCapturePermissionState(): DockWindowLevel: %d, "
+ "NormalWindowLevel: %d",
+ windowLevelDock, windowLevelNormal);
+
+ int32_t thisPid = [[NSProcessInfo processInfo] processIdentifier];
+
+ CFIndex windowCount = CFArrayGetCount(windowArray);
+ LOG("GetScreenCapturePermissionState() returned %ld windows", windowCount);
+ if (windowCount == 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (CFIndex i = 0; i < windowCount; i++) {
+ CFDictionaryRef windowDict =
+ reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windowArray, i));
+
+ // Get the window owner's PID
+ int32_t windowOwnerPid = -1;
+ CFNumberRef windowPidRef =
+ reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowOwnerPID));
+ if (!windowPidRef || !CFNumberGetValue(windowPidRef, kCFNumberIntType, &windowOwnerPid)) {
+ LOG("GetScreenCapturePermissionState() ERROR: failed to get window owner");
+ continue;
+ }
+
+ // Our own window names are always readable and
+ // therefore not relevant to the heuristic.
+ if (thisPid == windowOwnerPid) {
+ continue;
+ }
+
+ CFStringRef windowName =
+ reinterpret_cast<CFStringRef>(CFDictionaryGetValue(windowDict, kCGWindowName));
+ if (!windowName) {
+ continue;
+ }
+
+ CFNumberRef windowLayerRef =
+ reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowLayer));
+ int32_t windowLayer;
+ if (!windowLayerRef || !CFNumberGetValue(windowLayerRef, kCFNumberIntType, &windowLayer)) {
+ LOG("GetScreenCapturePermissionState() ERROR: failed to get layer");
+ continue;
+ }
+
+ // If we have a window name and the window is in the dock or normal window
+ // level, and for another process, assume we have screen recording access.
+ LOG("GetScreenCapturePermissionState(): windowLayer: %d", windowLayer);
+ if (windowLayer == windowLevelDock || windowLayer == windowLevelNormal) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ LOG("screen authorization status: authorized");
+ return NS_OK;
+ }
+ }
+
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
+ LOG("screen authorization status: not authorized");
+ return NS_OK;
+ }
+
+ LOG("GetScreenCapturePermissionState(): nothing to do, not on 10.15+");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsCocoaUtils::RequestVideoCapturePermission(RefPtr<Promise>& aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return nsCocoaUtils::RequestCapturePermission(AVMediaTypeVideo, aPromise, sVideoCapturePromises,
+ VideoCompletionHandler);
+}
+
+nsresult nsCocoaUtils::RequestAudioCapturePermission(RefPtr<Promise>& aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return nsCocoaUtils::RequestCapturePermission(AVMediaTypeAudio, aPromise, sAudioCapturePromises,
+ AudioCompletionHandler);
+}
+
+//
+// Stores |aPromise| on |aPromiseList| and starts an asynchronous media
+// capture request for the given media type |aType|. If we are already
+// waiting for a capture request for this media type, don't start a new
+// request. |aHandler| is invoked on an arbitrary dispatch queue when the
+// request completes and must resolve any waiting Promises on the main
+// thread.
+//
+nsresult nsCocoaUtils::RequestCapturePermission(AVMediaType aType, RefPtr<Promise>& aPromise,
+ PromiseArray& aPromiseList,
+ void (^aHandler)(BOOL granted)) {
+ MOZ_ASSERT(aType == AVMediaTypeVideo || aType == AVMediaTypeAudio);
+#if defined(MAC_OS_X_VERSION_10_14)
+ // Ensure our enum constants match. We can only do this when
+ // compiling on 10.14+ because AVAuthorizationStatus is
+ // prohibited by preprocessor checks on earlier OS versions.
+ if (@available(macOS 10.14, *)) {
+ static_assert(
+ (int)GeckoAVAuthorizationStatusNotDetermined == (int)AVAuthorizationStatusNotDetermined,
+ "GeckoAVAuthorizationStatusNotDetermined does not match");
+ static_assert((int)GeckoAVAuthorizationStatusRestricted == (int)AVAuthorizationStatusRestricted,
+ "GeckoAVAuthorizationStatusRestricted does not match");
+ static_assert((int)GeckoAVAuthorizationStatusDenied == (int)AVAuthorizationStatusDenied,
+ "GeckoAVAuthorizationStatusDenied does not match");
+ static_assert((int)GeckoAVAuthorizationStatusAuthorized == (int)AVAuthorizationStatusAuthorized,
+ "GeckoAVAuthorizationStatusAuthorized does not match");
+ }
+#endif
+ LOG("RequestCapturePermission(%s)", AVMediaTypeToString(aType));
+
+ // Only attempt to request authorization on 10.14+.
+ if (@available(macOS 10.14, *)) {
+ sMediaCaptureMutex.Lock();
+
+ // Initialize our list of promises on first invocation
+ if (aPromiseList == nullptr) {
+ aPromiseList = new nsTArray<RefPtr<Promise>>;
+ ClearOnShutdown(&aPromiseList);
+ }
+
+ aPromiseList->AppendElement(aPromise);
+ size_t nPromises = aPromiseList->Length();
+
+ sMediaCaptureMutex.Unlock();
+
+ LOG("RequestCapturePermission(%s): %ld promise(s) unresolved", AVMediaTypeToString(aType),
+ nPromises);
+
+ // If we had one or more more existing promises waiting to be resolved
+ // by the completion handler, we don't need to start another request.
+ if (nPromises > 1) {
+ return NS_OK;
+ }
+
+ // Start the request
+ [AVCaptureDevice requestAccessForMediaType:aType completionHandler:aHandler];
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+//
+// Audio capture request completion handler. Called from an arbitrary
+// dispatch queue.
+//
+void (^nsCocoaUtils::AudioCompletionHandler)(BOOL) = ^void(BOOL granted) {
+ nsCocoaUtils::ResolveAudioCapturePromises(granted);
+};
+
+//
+// Video capture request completion handler. Called from an arbitrary
+// dispatch queue.
+//
+void (^nsCocoaUtils::VideoCompletionHandler)(BOOL) = ^void(BOOL granted) {
+ nsCocoaUtils::ResolveVideoCapturePromises(granted);
+};
+
+void nsCocoaUtils::ResolveMediaCapturePromises(bool aGranted, PromiseArray& aPromiseList) {
+ StaticMutexAutoLock lock(sMediaCaptureMutex);
+
+ // Remove each promise from the list and resolve it.
+ while (aPromiseList->Length() > 0) {
+ RefPtr<Promise> promise = aPromiseList->PopLastElement();
+
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
+ "ResolveMediaAccessPromise",
+ [aGranted, aPromise = std::move(promise)]() { aPromise->MaybeResolve(aGranted); }));
+ NS_DispatchToMainThread(runnable.forget());
+ }
+}
+
+void nsCocoaUtils::ResolveAudioCapturePromises(bool aGranted) {
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction("ResolveAudioCapturePromise", [aGranted]() {
+ ResolveMediaCapturePromises(aGranted, sAudioCapturePromises);
+ }));
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+//
+// Attempt to trigger a dialog requesting permission to record the screen.
+// Unlike with the camera and microphone, there is no API to request permission
+// to record the screen or to receive a callback when permission is explicitly
+// allowed or denied. Here we attempt to trigger the dialog by attempting to
+// capture a 1x1 pixel section of the screen. The permission dialog is not
+// guaranteed to be displayed because the user may have already been prompted
+// in which case macOS does not display the dialog again.
+//
+nsresult nsCocoaUtils::MaybeRequestScreenCapturePermission() {
+ LOG("MaybeRequestScreenCapturePermission()");
+ AutoCFRelease<CGImageRef> image =
+ CGDisplayCreateImageForRect(kCGDirectMainDisplay, CGRectMake(0, 0, 1, 1));
+ return NS_OK;
+}
+
+void nsCocoaUtils::ResolveVideoCapturePromises(bool aGranted) {
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction("ResolveVideoCapturePromise", [aGranted]() {
+ ResolveMediaCapturePromises(aGranted, sVideoCapturePromises);
+ }));
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+static PanGestureInput::PanGestureType PanGestureTypeForEvent(NSEvent* aEvent) {
+ switch ([aEvent phase]) {
+ case NSEventPhaseMayBegin:
+ return PanGestureInput::PANGESTURE_MAYSTART;
+ case NSEventPhaseCancelled:
+ return PanGestureInput::PANGESTURE_CANCELLED;
+ case NSEventPhaseBegan:
+ return PanGestureInput::PANGESTURE_START;
+ case NSEventPhaseChanged:
+ return PanGestureInput::PANGESTURE_PAN;
+ case NSEventPhaseEnded:
+ return PanGestureInput::PANGESTURE_END;
+ case NSEventPhaseNone:
+ switch ([aEvent momentumPhase]) {
+ case NSEventPhaseBegan:
+ return PanGestureInput::PANGESTURE_MOMENTUMSTART;
+ case NSEventPhaseChanged:
+ return PanGestureInput::PANGESTURE_MOMENTUMPAN;
+ case NSEventPhaseEnded:
+ return PanGestureInput::PANGESTURE_MOMENTUMEND;
+ default:
+ NS_ERROR("unexpected event phase");
+ return PanGestureInput::PANGESTURE_PAN;
+ }
+ default:
+ NS_ERROR("unexpected event phase");
+ return PanGestureInput::PANGESTURE_PAN;
+ }
+}
+
+bool static ShouldConsiderStartingSwipeFromEvent(NSEvent* anEvent) {
+ // Only initiate horizontal tracking for gestures that have just begun --
+ // otherwise a scroll to one side of the page can have a swipe tacked on
+ // to it.
+ // [NSEvent isSwipeTrackingFromScrollEventsEnabled] checks whether the
+ // AppleEnableSwipeNavigateWithScrolls global preference is set. If it isn't,
+ // fluid swipe tracking is disabled, and a horizontal two-finger gesture is
+ // always a scroll (even in Safari). This preference can't (currently) be set
+ // from the Preferences UI -- only using 'defaults write'.
+ NSEventPhase eventPhase = [anEvent phase];
+ return [anEvent type] == NSEventTypeScrollWheel && eventPhase == NSEventPhaseBegan &&
+ [anEvent hasPreciseScrollingDeltas] && [NSEvent isSwipeTrackingFromScrollEventsEnabled];
+}
+
+PanGestureInput nsCocoaUtils::CreatePanGestureEvent(NSEvent* aNativeEvent, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint,
+ const ScreenPoint& aPreciseDelta,
+ const gfx::IntPoint& aLineOrPageDelta,
+ Modifiers aModifiers) {
+ PanGestureInput::PanGestureType type = PanGestureTypeForEvent(aNativeEvent);
+ // Always force zero deltas on event types that shouldn't cause any scrolling,
+ // so that we don't dispatch DOM wheel events for them.
+ bool shouldIgnoreDeltas =
+ type == PanGestureInput::PANGESTURE_MAYSTART || type == PanGestureInput::PANGESTURE_CANCELLED;
+
+ PanGestureInput panEvent(
+ type, aTimeStamp, aPanStartPoint, !shouldIgnoreDeltas ? aPreciseDelta : ScreenPoint(),
+ aModifiers,
+ PanGestureInput::IsEligibleForSwipe(ShouldConsiderStartingSwipeFromEvent(aNativeEvent)));
+
+ if (!shouldIgnoreDeltas) {
+ panEvent.SetLineOrPageDeltas(aLineOrPageDelta.x, aLineOrPageDelta.y);
+ }
+
+ return panEvent;
+}
+
+bool nsCocoaUtils::IsValidPasteboardType(NSString* aAvailableType, bool aAllowFileURL) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Prevent exposing fileURL for non-fileURL type.
+ // We need URL provided by dropped webloc file, but don't need file's URL.
+ // kUTTypeFileURL is returned by [NSPasteboard availableTypeFromArray:] for
+ // kPublicUrlPboardType, since it conforms to kPublicUrlPboardType.
+ bool isValid = true;
+ if (!aAllowFileURL &&
+ [aAvailableType isEqualToString:[UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]]) {
+ isValid = false;
+ }
+
+ return isValid;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+NSString* nsCocoaUtils::GetStringForTypeFromPasteboardItem(NSPasteboardItem* aItem,
+ const NSString* aType,
+ bool aAllowFileURL) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* availableType =
+ [aItem availableTypeFromArray:[NSArray arrayWithObjects:(id)aType, nil]];
+ if (availableType && IsValidPasteboardType(availableType, aAllowFileURL)) {
+ return [aItem stringForType:(id)availableType];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSString* nsCocoaUtils::GetFilePathFromPasteboardItem(NSPasteboardItem* aItem) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* urlString = GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL], true);
+ if (urlString) {
+ NSURL* url = [NSURL URLWithString:urlString];
+ if (url) {
+ return [url path];
+ }
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSString* nsCocoaUtils::GetTitleForURLFromPasteboardItem(NSPasteboardItem* item) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* name = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ item, [UTIHelper stringFromPboardType:kPublicUrlNamePboardType]);
+ if (name) {
+ return name;
+ }
+
+ NSString* filePath = nsCocoaUtils::GetFilePathFromPasteboardItem(item);
+ if (filePath) {
+ return [filePath lastPathComponent];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(nsITransferable* aTransferable,
+ const nsCString& aFlavor,
+ NSPasteboardItem* aItem) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aTransferable || !aItem) {
+ return;
+ }
+
+ MOZ_LOG(gCocoaUtilsLog, LogLevel::Info,
+ ("nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem: looking for pasteboard data of "
+ "type %s\n",
+ aFlavor.get()));
+
+ if (aFlavor.EqualsLiteral(kFileMime)) {
+ NSString* filePath = nsCocoaUtils::GetFilePathFromPasteboardItem(aItem);
+ if (!filePath) {
+ return;
+ }
+
+ unsigned int stringLength = [filePath length];
+ unsigned int dataLength = (stringLength + 1) * sizeof(char16_t); // in bytes
+ char16_t* clipboardDataPtr = (char16_t*)malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+
+ [filePath getCharacters:reinterpret_cast<unichar*>(clipboardDataPtr)];
+ clipboardDataPtr[stringLength] = 0; // null terminate
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(nsDependentString(clipboardDataPtr), true, getter_AddRefs(file));
+ free(clipboardDataPtr);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ aTransferable->SetTransferData(aFlavor.get(), file);
+ return;
+ }
+
+ if (aFlavor.EqualsLiteral(kCustomTypesMime)) {
+ NSString* availableType =
+ [aItem availableTypeFromArray:[NSArray arrayWithObject:kMozCustomTypesPboardType]];
+ if (!availableType || !nsCocoaUtils::IsValidPasteboardType(availableType, false)) {
+ return;
+ }
+ NSData* pasteboardData = [aItem dataForType:availableType];
+ if (!pasteboardData) {
+ return;
+ }
+
+ unsigned int dataLength = [pasteboardData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+ [pasteboardData getBytes:clipboardDataPtr length:dataLength];
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(aFlavor, clipboardDataPtr, dataLength,
+ getter_AddRefs(genericDataWrapper));
+
+ aTransferable->SetTransferData(aFlavor.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ return;
+ }
+
+ NSString* pString = nil;
+ if (aFlavor.EqualsLiteral(kTextMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeString]);
+ } else if (aFlavor.EqualsLiteral(kHTMLMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeHTML]);
+ } else if (aFlavor.EqualsLiteral(kURLMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:kPublicUrlPboardType]);
+ if (pString) {
+ NSString* title = GetTitleForURLFromPasteboardItem(aItem);
+ if (!title) {
+ title = pString;
+ }
+ pString = [NSString stringWithFormat:@"%@\n%@", pString, title];
+ }
+ } else if (aFlavor.EqualsLiteral(kURLDataMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:kPublicUrlPboardType]);
+ } else if (aFlavor.EqualsLiteral(kURLDescriptionMime)) {
+ pString = GetTitleForURLFromPasteboardItem(aItem);
+ } else if (aFlavor.EqualsLiteral(kRTFMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeRTF]);
+ }
+ if (pString) {
+ NSData* stringData;
+ bool isRTF = aFlavor.EqualsLiteral(kRTFMime);
+ if (isRTF) {
+ stringData = [pString dataUsingEncoding:NSASCIIStringEncoding];
+ } else {
+ stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding];
+ }
+ unsigned int dataLength = [stringData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+ [stringData getBytes:clipboardDataPtr length:dataLength];
+
+ // The DOM only wants LF, so convert from MacOS line endings to DOM line endings.
+ int32_t signedDataLength = dataLength;
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &clipboardDataPtr, &signedDataLength);
+ dataLength = signedDataLength;
+
+ // skip BOM (Byte Order Mark to distinguish little or big endian)
+ char16_t* clipboardDataPtrNoBOM = (char16_t*)clipboardDataPtr;
+ if ((dataLength > 2) &&
+ ((clipboardDataPtrNoBOM[0] == 0xFEFF) || (clipboardDataPtrNoBOM[0] == 0xFFFE))) {
+ dataLength -= sizeof(char16_t);
+ clipboardDataPtrNoBOM += 1;
+ }
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(aFlavor, clipboardDataPtrNoBOM, dataLength,
+ getter_AddRefs(genericDataWrapper));
+ aTransferable->SetTransferData(aFlavor.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ return;
+ }
+
+ // We have never supported this on Mac OS X, we should someday. Normally dragging images
+ // in is accomplished with a file path drag instead of the image data itself.
+ /*
+ if (aFlavor.EqualsLiteral(kPNGImageMime) || aFlavor.EqualsLiteral(kJPEGImageMime) ||
+ aFlavor.EqualsLiteral(kJPGImageMime) || aFlavor.EqualsLiteral(kGIFImageMime)) {
+
+ }
+ */
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}