summaryrefslogtreecommitdiffstats
path: root/widget/cocoa
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa')
-rw-r--r--widget/cocoa/AppearanceOverride.h19
-rw-r--r--widget/cocoa/AppearanceOverride.mm100
-rw-r--r--widget/cocoa/CFTypeRefPtr.h194
-rw-r--r--widget/cocoa/CustomCocoaEvents.h18
-rw-r--r--widget/cocoa/DesktopBackgroundImage.h19
-rw-r--r--widget/cocoa/DesktopBackgroundImage.mm68
-rw-r--r--widget/cocoa/GfxInfo.h98
-rw-r--r--widget/cocoa/GfxInfo.mm495
-rw-r--r--widget/cocoa/MOZIconHelper.h34
-rw-r--r--widget/cocoa/MOZIconHelper.mm64
-rw-r--r--widget/cocoa/MOZMenuOpeningCoordinator.h53
-rw-r--r--widget/cocoa/MOZMenuOpeningCoordinator.mm216
-rw-r--r--widget/cocoa/MacThemeGeometryType.h21
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMac.h47
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMac.mm183
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h60
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm172
-rw-r--r--widget/cocoa/MediaKeysEventSourceFactory.cpp23
-rw-r--r--widget/cocoa/NativeKeyBindings.h67
-rw-r--r--widget/cocoa/NativeKeyBindings.mm605
-rw-r--r--widget/cocoa/NativeMenuMac.h93
-rw-r--r--widget/cocoa/NativeMenuMac.mm394
-rw-r--r--widget/cocoa/NativeMenuSupport.mm32
-rw-r--r--widget/cocoa/OSXNotificationCenter.h56
-rw-r--r--widget/cocoa/OSXNotificationCenter.mm556
-rw-r--r--widget/cocoa/SDKDeclarations.h133
-rw-r--r--widget/cocoa/ScreenHelperCocoa.h34
-rw-r--r--widget/cocoa/ScreenHelperCocoa.mm178
-rw-r--r--widget/cocoa/TextInputHandler.h1283
-rw-r--r--widget/cocoa/TextInputHandler.mm5073
-rw-r--r--widget/cocoa/TextRecognition.mm109
-rw-r--r--widget/cocoa/VibrancyManager.h93
-rw-r--r--widget/cocoa/VibrancyManager.mm147
-rw-r--r--widget/cocoa/ViewRegion.h54
-rw-r--r--widget/cocoa/ViewRegion.mm66
-rw-r--r--widget/cocoa/WidgetTraceEvent.mm79
-rw-r--r--widget/cocoa/components.conf168
-rw-r--r--widget/cocoa/crashtests/373122-1-inner.html39
-rw-r--r--widget/cocoa/crashtests/373122-1.html9
-rw-r--r--widget/cocoa/crashtests/397209-1.html7
-rw-r--r--widget/cocoa/crashtests/403296-1.xhtml10
-rw-r--r--widget/cocoa/crashtests/419737-1.html8
-rw-r--r--widget/cocoa/crashtests/435223-1.html8
-rw-r--r--widget/cocoa/crashtests/444260-1.xhtml3
-rw-r--r--widget/cocoa/crashtests/444864-1.html6
-rw-r--r--widget/cocoa/crashtests/449111-1.html4
-rw-r--r--widget/cocoa/crashtests/460349-1.xhtml4
-rw-r--r--widget/cocoa/crashtests/460387-1.html2
-rw-r--r--widget/cocoa/crashtests/464589-1.html20
-rw-r--r--widget/cocoa/crashtests/crashtests.list11
-rw-r--r--widget/cocoa/cursors/arrowN.pngbin0 -> 253 bytes
-rw-r--r--widget/cocoa/cursors/arrowN@2x.pngbin0 -> 614 bytes
-rw-r--r--widget/cocoa/cursors/arrowS.pngbin0 -> 250 bytes
-rw-r--r--widget/cocoa/cursors/arrowS@2x.pngbin0 -> 609 bytes
-rw-r--r--widget/cocoa/cursors/cell.pngbin0 -> 264 bytes
-rw-r--r--widget/cocoa/cursors/cell@2x.pngbin0 -> 639 bytes
-rw-r--r--widget/cocoa/cursors/colResize.pngbin0 -> 320 bytes
-rw-r--r--widget/cocoa/cursors/colResize@2x.pngbin0 -> 825 bytes
-rw-r--r--widget/cocoa/cursors/help.pngbin0 -> 713 bytes
-rw-r--r--widget/cocoa/cursors/help@2x.pngbin0 -> 1679 bytes
-rw-r--r--widget/cocoa/cursors/move.pngbin0 -> 281 bytes
-rw-r--r--widget/cocoa/cursors/move@2x.pngbin0 -> 619 bytes
-rw-r--r--widget/cocoa/cursors/rowResize.pngbin0 -> 329 bytes
-rw-r--r--widget/cocoa/cursors/rowResize@2x.pngbin0 -> 843 bytes
-rw-r--r--widget/cocoa/cursors/sizeNE.pngbin0 -> 274 bytes
-rw-r--r--widget/cocoa/cursors/sizeNE@2x.pngbin0 -> 775 bytes
-rw-r--r--widget/cocoa/cursors/sizeNESW.pngbin0 -> 295 bytes
-rw-r--r--widget/cocoa/cursors/sizeNESW@2x.pngbin0 -> 948 bytes
-rw-r--r--widget/cocoa/cursors/sizeNS.pngbin0 -> 279 bytes
-rw-r--r--widget/cocoa/cursors/sizeNS@2x.pngbin0 -> 658 bytes
-rw-r--r--widget/cocoa/cursors/sizeNW.pngbin0 -> 274 bytes
-rw-r--r--widget/cocoa/cursors/sizeNW@2x.pngbin0 -> 771 bytes
-rw-r--r--widget/cocoa/cursors/sizeNWSE.pngbin0 -> 288 bytes
-rw-r--r--widget/cocoa/cursors/sizeNWSE@2x.pngbin0 -> 947 bytes
-rw-r--r--widget/cocoa/cursors/sizeSE.pngbin0 -> 264 bytes
-rw-r--r--widget/cocoa/cursors/sizeSE@2x.pngbin0 -> 783 bytes
-rw-r--r--widget/cocoa/cursors/sizeSW.pngbin0 -> 268 bytes
-rw-r--r--widget/cocoa/cursors/sizeSW@2x.pngbin0 -> 783 bytes
-rw-r--r--widget/cocoa/cursors/vtIBeam.pngbin0 -> 104 bytes
-rw-r--r--widget/cocoa/cursors/vtIBeam@2x.pngbin0 -> 331 bytes
-rw-r--r--widget/cocoa/cursors/zoomIn.pngbin0 -> 648 bytes
-rw-r--r--widget/cocoa/cursors/zoomIn@2x.pngbin0 -> 1702 bytes
-rw-r--r--widget/cocoa/cursors/zoomOut.pngbin0 -> 641 bytes
-rw-r--r--widget/cocoa/cursors/zoomOut@2x.pngbin0 -> 1693 bytes
-rw-r--r--widget/cocoa/docs/index.md9
-rw-r--r--widget/cocoa/docs/macos-apis.md188
-rw-r--r--widget/cocoa/docs/sdks.md227
-rw-r--r--widget/cocoa/metrics.yaml28
-rw-r--r--widget/cocoa/moz.build182
-rw-r--r--widget/cocoa/mozView.h62
-rw-r--r--widget/cocoa/nsAppShell.h92
-rw-r--r--widget/cocoa/nsAppShell.mm1120
-rw-r--r--widget/cocoa/nsBidiKeyboard.h23
-rw-r--r--widget/cocoa/nsBidiKeyboard.mm38
-rw-r--r--widget/cocoa/nsChangeObserver.h71
-rw-r--r--widget/cocoa/nsChildView.h591
-rw-r--r--widget/cocoa/nsChildView.mm4926
-rw-r--r--widget/cocoa/nsClipboard.h63
-rw-r--r--widget/cocoa/nsClipboard.mm776
-rw-r--r--widget/cocoa/nsCocoaFeatures.h57
-rw-r--r--widget/cocoa/nsCocoaFeatures.mm220
-rw-r--r--widget/cocoa/nsCocoaUtils.h574
-rw-r--r--widget/cocoa/nsCocoaUtils.mm1819
-rw-r--r--widget/cocoa/nsCocoaWindow.h493
-rw-r--r--widget/cocoa/nsCocoaWindow.mm4259
-rw-r--r--widget/cocoa/nsColorPicker.h41
-rw-r--r--widget/cocoa/nsColorPicker.mm158
-rw-r--r--widget/cocoa/nsCursorManager.h60
-rw-r--r--widget/cocoa/nsCursorManager.mm317
-rw-r--r--widget/cocoa/nsDeviceContextSpecX.h53
-rw-r--r--widget/cocoa/nsDeviceContextSpecX.mm303
-rw-r--r--widget/cocoa/nsDragService.h57
-rw-r--r--widget/cocoa/nsDragService.mm477
-rw-r--r--widget/cocoa/nsFilePicker.h73
-rw-r--r--widget/cocoa/nsFilePicker.mm639
-rw-r--r--widget/cocoa/nsLookAndFeel.h43
-rw-r--r--widget/cocoa/nsLookAndFeel.mm691
-rw-r--r--widget/cocoa/nsMacCursor.h128
-rw-r--r--widget/cocoa/nsMacCursor.mm367
-rw-r--r--widget/cocoa/nsMacDockSupport.h35
-rw-r--r--widget/cocoa/nsMacDockSupport.mm419
-rw-r--r--widget/cocoa/nsMacFinderProgress.h24
-rw-r--r--widget/cocoa/nsMacFinderProgress.mm87
-rw-r--r--widget/cocoa/nsMacSharingService.h22
-rw-r--r--widget/cocoa/nsMacSharingService.mm206
-rw-r--r--widget/cocoa/nsMacUserActivityUpdater.h23
-rw-r--r--widget/cocoa/nsMacUserActivityUpdater.mm64
-rw-r--r--widget/cocoa/nsMacWebAppUtils.h22
-rw-r--r--widget/cocoa/nsMacWebAppUtils.mm90
-rw-r--r--widget/cocoa/nsMenuBarX.h153
-rw-r--r--widget/cocoa/nsMenuBarX.mm1072
-rw-r--r--widget/cocoa/nsMenuGroupOwnerX.h95
-rw-r--r--widget/cocoa/nsMenuGroupOwnerX.mm226
-rw-r--r--widget/cocoa/nsMenuItemIconX.h70
-rw-r--r--widget/cocoa/nsMenuItemIconX.mm170
-rw-r--r--widget/cocoa/nsMenuItemX.h105
-rw-r--r--widget/cocoa/nsMenuItemX.mm401
-rw-r--r--widget/cocoa/nsMenuParentX.h31
-rw-r--r--widget/cocoa/nsMenuUtilsX.h50
-rw-r--r--widget/cocoa/nsMenuUtilsX.mm294
-rw-r--r--widget/cocoa/nsMenuX.h288
-rw-r--r--widget/cocoa/nsMenuX.mm1403
-rw-r--r--widget/cocoa/nsNativeThemeCocoa.h419
-rw-r--r--widget/cocoa/nsNativeThemeCocoa.mm3444
-rw-r--r--widget/cocoa/nsNativeThemeColors.h72
-rw-r--r--widget/cocoa/nsPIWidgetCocoa.idl37
-rw-r--r--widget/cocoa/nsPrintDialogX.h59
-rw-r--r--widget/cocoa/nsPrintDialogX.mm590
-rw-r--r--widget/cocoa/nsPrintSettingsServiceX.h33
-rw-r--r--widget/cocoa/nsPrintSettingsServiceX.mm74
-rw-r--r--widget/cocoa/nsPrintSettingsX.h102
-rw-r--r--widget/cocoa/nsPrintSettingsX.mm362
-rw-r--r--widget/cocoa/nsSandboxViolationSink.h36
-rw-r--r--widget/cocoa/nsSandboxViolationSink.mm107
-rw-r--r--widget/cocoa/nsSound.h25
-rw-r--r--widget/cocoa/nsSound.mm69
-rw-r--r--widget/cocoa/nsStandaloneNativeMenu.h27
-rw-r--r--widget/cocoa/nsStandaloneNativeMenu.mm79
-rw-r--r--widget/cocoa/nsSystemStatusBarCocoa.h40
-rw-r--r--widget/cocoa/nsSystemStatusBarCocoa.mm69
-rw-r--r--widget/cocoa/nsToolkit.h49
-rw-r--r--widget/cocoa/nsToolkit.mm252
-rw-r--r--widget/cocoa/nsTouchBar.h136
-rw-r--r--widget/cocoa/nsTouchBar.mm605
-rw-r--r--widget/cocoa/nsTouchBarInput.h90
-rw-r--r--widget/cocoa/nsTouchBarInput.mm245
-rw-r--r--widget/cocoa/nsTouchBarInputIcon.h71
-rw-r--r--widget/cocoa/nsTouchBarInputIcon.mm133
-rw-r--r--widget/cocoa/nsTouchBarNativeAPIDefines.h67
-rw-r--r--widget/cocoa/nsTouchBarUpdater.h23
-rw-r--r--widget/cocoa/nsTouchBarUpdater.mm116
-rw-r--r--widget/cocoa/nsUserIdleServiceX.h30
-rw-r--r--widget/cocoa/nsUserIdleServiceX.mm58
-rw-r--r--widget/cocoa/nsWidgetFactory.h44
-rw-r--r--widget/cocoa/nsWidgetFactory.mm124
-rw-r--r--widget/cocoa/nsWindowMap.h60
-rw-r--r--widget/cocoa/nsWindowMap.mm281
-rw-r--r--widget/cocoa/resources/MainMenu.nib/classes.nib4
-rw-r--r--widget/cocoa/resources/MainMenu.nib/info.nib21
-rw-r--r--widget/cocoa/resources/MainMenu.nib/keyedobjects.nibbin0 -> 1877 bytes
180 files changed, 43863 insertions, 0 deletions
diff --git a/widget/cocoa/AppearanceOverride.h b/widget/cocoa/AppearanceOverride.h
new file mode 100644
index 0000000000..c826323c51
--- /dev/null
+++ b/widget/cocoa/AppearanceOverride.h
@@ -0,0 +1,19 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef AppearanceOverride_h
+#define AppearanceOverride_h
+
+#import <Cocoa/Cocoa.h>
+
+// Implements support for the browser.theme.toolbar-theme pref.
+// Use MOZGlobalAppearance.sharedInstance.effectiveAppearance
+// in all places where you would like the global override to be respected. The effectiveAppearance
+// property can be key-value observed.
+@interface MOZGlobalAppearance : NSObject <NSAppearanceCustomization>
+@property(class, readonly) MOZGlobalAppearance* sharedInstance;
+@end
+
+#endif
diff --git a/widget/cocoa/AppearanceOverride.mm b/widget/cocoa/AppearanceOverride.mm
new file mode 100644
index 0000000000..3f3d5e38e9
--- /dev/null
+++ b/widget/cocoa/AppearanceOverride.mm
@@ -0,0 +1,100 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "AppearanceOverride.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+#include "nsXULAppAPI.h"
+#include "SDKDeclarations.h"
+
+static void ToolbarThemePrefChanged(const char* aPref, void* aUserInfo);
+
+@interface MOZGlobalAppearance ()
+@property NSInteger toolbarTheme;
+@end
+
+@implementation MOZGlobalAppearance
+
++ (MOZGlobalAppearance*)sharedInstance {
+ static MOZGlobalAppearance* sInstance = nil;
+ if (!sInstance) {
+ sInstance = [[MOZGlobalAppearance alloc] init];
+ if (XRE_IsParentProcess()) {
+ mozilla::Preferences::RegisterCallbackAndCall(
+ &ToolbarThemePrefChanged,
+ nsDependentCString(mozilla::StaticPrefs::GetPrefName_browser_theme_toolbar_theme()));
+ }
+ }
+ return sInstance;
+}
+
++ (NSSet*)keyPathsForValuesAffectingAppearance {
+ return [NSSet setWithObjects:@"toolbarTheme", nil];
+}
+
+- (NSAppearance*)appearance {
+ if (@available(macOS 10.14, *)) {
+ switch (self.toolbarTheme) { // Value for browser.theme.toolbar-theme pref
+ case 0: // Dark
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
+ case 1: // Light
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+ case 2: // System
+ default:
+ break;
+ }
+ }
+ // nil means "no override".
+ return nil;
+}
+
+- (void)setAppearance:(NSAppearance*)aAppearance {
+ // ignored
+}
+
+- (NSApplication*)_app {
+ return NSApp;
+}
+
++ (NSSet*)keyPathsForValuesAffectingEffectiveAppearance {
+ if (@available(macOS 10.14, *)) {
+ // Automatically notify any key-value observers of our effectiveAppearance property whenever the
+ // pref or the NSApp's effectiveAppearance change.
+ return [NSSet setWithObjects:@"toolbarTheme", @"_app.effectiveAppearance", nil];
+ }
+ return [NSSet set];
+}
+
+- (NSAppearance*)effectiveAppearance {
+ if (@available(macOS 10.14, *)) {
+ switch (self.toolbarTheme) { // Value for browser.theme.toolbar-theme pref
+ case 0: // Dark
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
+ case 1: // Light
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+ case 2: // System
+ default:
+ // Use the NSApp effectiveAppearance. This is the system appearance.
+ return NSApp.effectiveAppearance;
+ }
+ }
+ // Use aqua on pre-10.14.
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+}
+
+@end
+
+static void ToolbarThemePrefChanged(const char* aPref, void* aUserInfo) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ MOZGlobalAppearance.sharedInstance.toolbarTheme =
+ mozilla::StaticPrefs::browser_theme_toolbar_theme();
+}
diff --git a/widget/cocoa/CFTypeRefPtr.h b/widget/cocoa/CFTypeRefPtr.h
new file mode 100644
index 0000000000..185355777e
--- /dev/null
+++ b/widget/cocoa/CFTypeRefPtr.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef CFTypeRefPtr_h
+#define CFTypeRefPtr_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DbgMacro.h"
+#include "mozilla/HashFunctions.h"
+
+// A smart pointer for CoreFoundation classes which does reference counting.
+//
+// Manual reference counting:
+//
+// UInt32 someNumber = 10;
+// CFNumberRef numberObject =
+// CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &someNumber);
+// // do something with numberObject
+// CFRelease(numberObject);
+//
+// Automatic reference counting using CFTypeRefPtr:
+//
+// UInt32 someNumber = 10;
+// auto numberObject =
+// CFTypeRefPtr<CFNumberRef>::WrapUnderCreateRule(
+// CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &someNumber));
+// // do something with numberObject
+// // no CFRelease
+
+template <class PtrT>
+class CFTypeRefPtr {
+ private:
+ void assign_with_CFRetain(PtrT aRawPtr) {
+ CFRetain(aRawPtr);
+ assign_assuming_CFRetain(aRawPtr);
+ }
+
+ void assign_assuming_CFRetain(PtrT aNewPtr) {
+ PtrT oldPtr = mRawPtr;
+ mRawPtr = aNewPtr;
+ if (oldPtr) {
+ CFRelease(oldPtr);
+ }
+ }
+
+ private:
+ PtrT mRawPtr;
+
+ public:
+ ~CFTypeRefPtr() {
+ if (mRawPtr) {
+ CFRelease(mRawPtr);
+ }
+ }
+
+ // Constructors
+
+ CFTypeRefPtr() : mRawPtr(nullptr) {}
+
+ CFTypeRefPtr(const CFTypeRefPtr<PtrT>& aSmartPtr)
+ : mRawPtr(aSmartPtr.mRawPtr) {
+ if (mRawPtr) {
+ CFRetain(mRawPtr);
+ }
+ }
+
+ CFTypeRefPtr(CFTypeRefPtr<PtrT>&& aRefPtr) : mRawPtr(aRefPtr.mRawPtr) {
+ aRefPtr.mRawPtr = nullptr;
+ }
+
+ MOZ_IMPLICIT CFTypeRefPtr(decltype(nullptr)) : mRawPtr(nullptr) {}
+
+ // There is no constructor from a raw pointer value.
+ // Use one of the static WrapUnder*Rule methods below instead.
+
+ static CFTypeRefPtr<PtrT> WrapUnderCreateRule(PtrT aRawPtr) {
+ CFTypeRefPtr<PtrT> ptr;
+ ptr.AssignUnderCreateRule(aRawPtr);
+ return ptr;
+ }
+
+ static CFTypeRefPtr<PtrT> WrapUnderGetRule(PtrT aRawPtr) {
+ CFTypeRefPtr<PtrT> ptr;
+ ptr.AssignUnderGetRule(aRawPtr);
+ return ptr;
+ }
+
+ // Assignment operators
+
+ CFTypeRefPtr<PtrT>& operator=(decltype(nullptr)) {
+ assign_assuming_CFRetain(nullptr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& operator=(const CFTypeRefPtr<PtrT>& aRhs) {
+ assign_with_CFRetain(aRhs.mRawPtr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& operator=(CFTypeRefPtr<PtrT>&& aRefPtr) {
+ assign_assuming_CFRetain(aRefPtr.mRawPtr);
+ aRefPtr.mRawPtr = nullptr;
+ return *this;
+ }
+
+ // There is no operator= for a raw pointer value.
+ // Use one of the AssignUnder*Rule methods below instead.
+
+ CFTypeRefPtr<PtrT>& AssignUnderCreateRule(PtrT aRawPtr) {
+ // Freshly-created objects come with a retain count of 1.
+ assign_assuming_CFRetain(aRawPtr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& AssignUnderGetRule(PtrT aRawPtr) {
+ assign_with_CFRetain(aRawPtr);
+ return *this;
+ }
+
+ // Other pointer operators
+
+ // This is the only way to get the raw pointer out of the smart pointer.
+ // There is no implicit conversion to a raw pointer.
+ PtrT get() const { return mRawPtr; }
+
+ // Don't allow implicit conversion of temporary CFTypeRefPtr to raw pointer,
+ // because the refcount might be one and the pointer will immediately become
+ // invalid.
+ operator PtrT() const&& = delete;
+ // Also don't allow implicit conversion of non-temporary CFTypeRefPtr.
+ operator PtrT() const& = delete;
+
+ // These let you null-check a pointer without calling get().
+ explicit operator bool() const { return !!mRawPtr; }
+};
+
+template <class PtrT>
+inline bool operator==(const CFTypeRefPtr<PtrT>& aLhs,
+ const CFTypeRefPtr<PtrT>& aRhs) {
+ return aLhs.get() == aRhs.get();
+}
+
+template <class PtrT>
+inline bool operator!=(const CFTypeRefPtr<PtrT>& aLhs,
+ const CFTypeRefPtr<PtrT>& aRhs) {
+ return !(aLhs == aRhs);
+}
+
+// Comparing an |CFTypeRefPtr| to |nullptr|
+
+template <class PtrT>
+inline bool operator==(const CFTypeRefPtr<PtrT>& aLhs, decltype(nullptr)) {
+ return aLhs.get() == nullptr;
+}
+
+template <class PtrT>
+inline bool operator==(decltype(nullptr), const CFTypeRefPtr<PtrT>& aRhs) {
+ return nullptr == aRhs.get();
+}
+
+template <class PtrT>
+inline bool operator!=(const CFTypeRefPtr<PtrT>& aLhs, decltype(nullptr)) {
+ return aLhs.get() != nullptr;
+}
+
+template <class PtrT>
+inline bool operator!=(decltype(nullptr), const CFTypeRefPtr<PtrT>& aRhs) {
+ return nullptr != aRhs.get();
+}
+
+// MOZ_DBG support
+
+template <class PtrT>
+std::ostream& operator<<(std::ostream& aOut, const CFTypeRefPtr<PtrT>& aObj) {
+ return mozilla::DebugValue(aOut, aObj.get());
+}
+
+// std::hash support (e.g. for unordered_map)
+namespace std {
+template <class PtrT>
+struct hash<CFTypeRefPtr<PtrT>> {
+ typedef CFTypeRefPtr<PtrT> argument_type;
+ typedef std::size_t result_type;
+ result_type operator()(argument_type const& aPtr) const {
+ return mozilla::HashGeneric(reinterpret_cast<uintptr_t>(aPtr.get()));
+ }
+};
+} // namespace std
+
+#endif /* CFTypeRefPtr_h */
diff --git a/widget/cocoa/CustomCocoaEvents.h b/widget/cocoa/CustomCocoaEvents.h
new file mode 100644
index 0000000000..3c02feb4b0
--- /dev/null
+++ b/widget/cocoa/CustomCocoaEvents.h
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/*
+ * This file defines constants to be used in the "subtype" field of
+ * NSEventTypeApplicationDefined type NSEvents.
+ */
+
+#ifndef WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_
+#define WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_
+
+// Empty event, just used for prodding the event loop into responding.
+const short kEventSubtypeNone = 0;
+// Tracer event, used for timing the event loop responsiveness.
+const short kEventSubtypeTrace = 1;
+
+#endif /* WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_ */
diff --git a/widget/cocoa/DesktopBackgroundImage.h b/widget/cocoa/DesktopBackgroundImage.h
new file mode 100644
index 0000000000..2fd7565369
--- /dev/null
+++ b/widget/cocoa/DesktopBackgroundImage.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
+#define WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
+
+class nsIFile;
+
+namespace mozilla {
+namespace widget {
+
+void SetDesktopImage(nsIFile* aImage);
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
diff --git a/widget/cocoa/DesktopBackgroundImage.mm b/widget/cocoa/DesktopBackgroundImage.mm
new file mode 100644
index 0000000000..5ebb7ea938
--- /dev/null
+++ b/widget/cocoa/DesktopBackgroundImage.mm
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+
+#include "mozilla/Logging.h"
+#include "nsCocoaUtils.h"
+#include "nsIFile.h"
+#include "DesktopBackgroundImage.h"
+
+#import <Foundation/Foundation.h>
+
+extern mozilla::LazyLogModule gCocoaUtilsLog;
+#undef LOG
+#define LOG(...) MOZ_LOG(gCocoaUtilsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+void SetDesktopImage(nsIFile* aImage) {
+ nsAutoCString imagePath;
+ nsresult rv = aImage->GetNativePath(imagePath);
+ if (NS_FAILED(rv)) {
+ LOG("%s ERROR: failed to get image path", __func__);
+ return;
+ }
+
+ bool exists = false;
+ rv = aImage->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ LOG("%s ERROR: file \"%s\" does not exist", __func__, imagePath.get());
+ return;
+ }
+
+ NSString* urlString = [NSString stringWithUTF8String:imagePath.get()];
+ if (!urlString) {
+ LOG("%s ERROR: null image path \"%s\"", __func__, imagePath.get());
+ return;
+ }
+
+ NSURL* url = [NSURL fileURLWithPath:urlString];
+ if (!url) {
+ LOG("%s ERROR: null image path URL \"%s\"", __func__, imagePath.get());
+ return;
+ }
+
+ // Only apply the background to the screen with focus
+ NSScreen* currentScreen = [NSScreen mainScreen];
+ if (!currentScreen) {
+ LOG("%s ERROR: got null NSScreen", __func__);
+ return;
+ }
+
+ // Use existing options for this screen
+ NSDictionary* screenOptions =
+ [[NSWorkspace sharedWorkspace] desktopImageOptionsForScreen:currentScreen];
+
+ NSError* error = nil;
+ if (![[NSWorkspace sharedWorkspace] setDesktopImageURL:url
+ forScreen:currentScreen
+ options:screenOptions
+ error:&error]) {
+ LOG("%s ERROR: setDesktopImageURL failed (%ld)", __func__, (long)[error code]);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/GfxInfo.h b/widget/cocoa/GfxInfo.h
new file mode 100644
index 0000000000..0c6e50c04c
--- /dev/null
+++ b/widget/cocoa/GfxInfo.h
@@ -0,0 +1,98 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef __mozilla_widget_GfxInfo_h__
+#define __mozilla_widget_GfxInfo_h__
+
+#include "GfxInfoBase.h"
+
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+class GfxInfo : public GfxInfoBase {
+ public:
+ GfxInfo();
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetEmbeddedInFirefoxReality(
+ bool* aEmbeddedInFirefoxReality) override;
+ NS_IMETHOD GetHasBattery(bool* aHasBattery) override;
+ NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override;
+ NS_IMETHOD GetTestType(nsAString& aTestType) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override;
+
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+ virtual nsresult Init() override;
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ virtual uint32_t OperatingSystemVersion() override { return mOSXVersion; }
+
+ protected:
+ virtual ~GfxInfo() {}
+
+ OperatingSystem GetOperatingSystem() override;
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr) override;
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
+
+ private:
+ void GetDeviceInfo();
+ void GetSelectedCityInfo();
+ void AddCrashReportAnnotations();
+
+ uint32_t mNumGPUsDetected;
+
+ uint32_t mAdapterRAM[2];
+ nsString mDeviceID[2];
+ nsString mDriverVersion[2];
+ nsString mDriverDate[2];
+ nsString mDeviceKey[2];
+
+ nsString mAdapterVendorID[2];
+ nsString mAdapterDeviceID[2];
+
+ uint32_t mOSXVersion;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_GfxInfo_h__ */
diff --git a/widget/cocoa/GfxInfo.mm b/widget/cocoa/GfxInfo.mm
new file mode 100644
index 0000000000..19944e9c02
--- /dev/null
+++ b/widget/cocoa/GfxInfo.mm
@@ -0,0 +1,495 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include <OpenGL/OpenGL.h>
+#include <OpenGL/CGLRenderers.h>
+
+#include "mozilla/ArrayUtils.h"
+
+#include "GfxInfo.h"
+#include "nsUnicharUtils.h"
+#include "nsExceptionHandler.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Preferences.h"
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+
+#include <algorithm>
+
+#import <Foundation/Foundation.h>
+#import <IOKit/IOKitLib.h>
+#import <Cocoa/Cocoa.h>
+
+#include "jsapi.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+GfxInfo::GfxInfo() : mNumGPUsDetected(0), mOSXVersion{0} { mAdapterRAM[0] = mAdapterRAM[1] = 0; }
+
+static OperatingSystem OSXVersionToOperatingSystem(uint32_t aOSXVersion) {
+ switch (nsCocoaFeatures::ExtractMajorVersion(aOSXVersion)) {
+ case 10:
+ switch (nsCocoaFeatures::ExtractMinorVersion(aOSXVersion)) {
+ case 6:
+ return OperatingSystem::OSX10_6;
+ case 7:
+ return OperatingSystem::OSX10_7;
+ case 8:
+ return OperatingSystem::OSX10_8;
+ case 9:
+ return OperatingSystem::OSX10_9;
+ case 10:
+ return OperatingSystem::OSX10_10;
+ case 11:
+ return OperatingSystem::OSX10_11;
+ case 12:
+ return OperatingSystem::OSX10_12;
+ case 13:
+ return OperatingSystem::OSX10_13;
+ case 14:
+ return OperatingSystem::OSX10_14;
+ case 15:
+ return OperatingSystem::OSX10_15;
+ case 16:
+ // Depending on the SDK version, we either get 10.16 or 11.0.
+ // Normalize this to 11.0.
+ return OperatingSystem::OSX11_0;
+ default:
+ break;
+ }
+ break;
+ case 11:
+ switch (nsCocoaFeatures::ExtractMinorVersion(aOSXVersion)) {
+ case 0:
+ return OperatingSystem::OSX11_0;
+ default:
+ break;
+ }
+ break;
+ }
+
+ return OperatingSystem::Unknown;
+}
+// The following three functions are derived from Chromium code
+static CFTypeRef SearchPortForProperty(io_registry_entry_t dspPort, CFStringRef propertyName) {
+ return IORegistryEntrySearchCFProperty(dspPort, kIOServicePlane, propertyName,
+ kCFAllocatorDefault,
+ kIORegistryIterateRecursively | kIORegistryIterateParents);
+}
+
+static uint32_t IntValueOfCFData(CFDataRef d) {
+ uint32_t value = 0;
+
+ if (d) {
+ const uint32_t* vp = reinterpret_cast<const uint32_t*>(CFDataGetBytePtr(d));
+ if (vp != NULL) value = *vp;
+ }
+
+ return value;
+}
+
+void GfxInfo::GetDeviceInfo() {
+ mNumGPUsDetected = 0;
+
+ CFMutableDictionaryRef pci_dev_dict = IOServiceMatching("IOPCIDevice");
+ io_iterator_t io_iter;
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, pci_dev_dict, &io_iter) !=
+ kIOReturnSuccess) {
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "Failed to detect any GPUs (couldn't enumerate IOPCIDevice services)");
+ return;
+ }
+
+ io_registry_entry_t entry = IO_OBJECT_NULL;
+ while ((entry = IOIteratorNext(io_iter)) != IO_OBJECT_NULL) {
+ constexpr uint32_t kClassCodeDisplayVGA = 0x30000;
+ CFTypeRef class_code_ref = SearchPortForProperty(entry, CFSTR("class-code"));
+ if (class_code_ref) {
+ const uint32_t class_code = IntValueOfCFData((CFDataRef)class_code_ref);
+ CFRelease(class_code_ref);
+
+ if (class_code == kClassCodeDisplayVGA) {
+ CFTypeRef vendor_id_ref = SearchPortForProperty(entry, CFSTR("vendor-id"));
+ if (vendor_id_ref) {
+ mAdapterVendorID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)vendor_id_ref));
+ CFRelease(vendor_id_ref);
+ }
+ CFTypeRef device_id_ref = SearchPortForProperty(entry, CFSTR("device-id"));
+ if (device_id_ref) {
+ mAdapterDeviceID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)device_id_ref));
+ CFRelease(device_id_ref);
+ }
+ ++mNumGPUsDetected;
+ }
+ }
+ IOObjectRelease(entry);
+ if (mNumGPUsDetected == 2) {
+ break;
+ }
+ }
+ IOObjectRelease(io_iter);
+
+#if defined(__aarch64__)
+ // If we found IOPCI VGA devices, don't look for AGXAccelerator devices
+ if (mNumGPUsDetected > 0) {
+ return;
+ }
+
+ CFMutableDictionaryRef agx_dev_dict = IOServiceMatching("AGXAccelerator");
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, agx_dev_dict, &io_iter) ==
+ kIOReturnSuccess) {
+ io_registry_entry_t entry = IO_OBJECT_NULL;
+ while ((entry = IOIteratorNext(io_iter)) != IO_OBJECT_NULL) {
+ CFTypeRef vendor_id_ref = SearchPortForProperty(entry, CFSTR("vendor-id"));
+ if (vendor_id_ref) {
+ mAdapterVendorID[mNumGPUsDetected].AppendPrintf("0x%04x",
+ IntValueOfCFData((CFDataRef)vendor_id_ref));
+ CFRelease(vendor_id_ref);
+ ++mNumGPUsDetected;
+ }
+ IOObjectRelease(entry);
+ }
+
+ IOObjectRelease(io_iter);
+ }
+#endif
+
+ MOZ_DIAGNOSTIC_ASSERT(mNumGPUsDetected > 0, "Failed to detect any GPUs");
+}
+
+nsresult GfxInfo::Init() {
+ nsresult rv = GfxInfoBase::Init();
+
+ // Calling CGLQueryRendererInfo causes us to switch to the discrete GPU
+ // even when we don't want to. We'll avoid doing so for now and just
+ // use the device ids.
+
+ GetDeviceInfo();
+
+ AddCrashReportAnnotations();
+
+ mOSXVersion = nsCocoaFeatures::macOSVersion();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+/* readonly attribute bool HasBattery; */
+NS_IMETHODIMP GfxInfo::GetHasBattery(bool* aHasBattery) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/* readonly attribute DOMString DWriteVersion; */
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) { return NS_ERROR_FAILURE; }
+
+/* readonly attribute DOMString cleartypeParameters; */
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) { return NS_ERROR_FAILURE; }
+
+/* readonly attribute DOMString windowProtocol; */
+NS_IMETHODIMP
+GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/* readonly attribute DOMString testType; */
+NS_IMETHODIMP
+GfxInfo::GetTestType(nsAString& aTestType) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/* readonly attribute DOMString adapterDescription; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ aAdapterDescription.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDescription2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDescription.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterRAM; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) {
+ *aAdapterRAM = mAdapterRAM[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterRAM2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ *aAdapterRAM = mAdapterRAM[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriver; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ aAdapterDriver.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriver2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriver.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVendor; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) {
+ aAdapterDriverVendor.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVendor2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverVendor.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVersion; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ aAdapterDriverVersion.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVersion2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverVersion.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverDate; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ aAdapterDriverDate.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverDate2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverDate.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterVendorID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ aAdapterVendorID = mAdapterVendorID[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterVendorID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterVendorID = mAdapterVendorID[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDeviceID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ aAdapterDeviceID = mAdapterDeviceID[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDeviceID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDeviceID = mAdapterDeviceID[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterSubsysID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) { return NS_ERROR_FAILURE; }
+
+/* readonly attribute DOMString adapterSubsysID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/* readonly attribute boolean isGPU2Active; */
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { return NS_ERROR_FAILURE; }
+
+void GfxInfo::AddCrashReportAnnotations() {
+ nsString deviceID, vendorID, driverVersion;
+ nsAutoCString narrowDeviceID, narrowVendorID, narrowDriverVersion;
+
+ GetAdapterDeviceID(deviceID);
+ CopyUTF16toUTF8(deviceID, narrowDeviceID);
+ GetAdapterVendorID(vendorID);
+ CopyUTF16toUTF8(vendorID, narrowVendorID);
+ GetAdapterDriverVersion(driverVersion);
+ CopyUTF16toUTF8(driverVersion, narrowDriverVersion);
+
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID, narrowVendorID);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID, narrowDeviceID);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDriverVersion,
+ narrowDriverVersion);
+}
+
+// We don't support checking driver versions on Mac.
+#define IMPLEMENT_MAC_DRIVER_BLOCKLIST(os, device, features, blockOn, ruleId) \
+ APPEND_TO_DRIVER_BLOCKLIST(os, device, features, blockOn, DRIVER_COMPARISON_IGNORED, \
+ V(0, 0, 0, 0), ruleId, "")
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (!sDriverInfo->Length()) {
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::RadeonX1000, nsIGfxInfo::FEATURE_OPENGL_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_MAC_RADEONX1000_NO_TEXTURE2D");
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::Geforce7300GT, nsIGfxInfo::FEATURE_WEBGL_OPENGL,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_MAC_7300_NO_WEBGL");
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(OperatingSystem::OSX, DeviceFamily::IntelHDGraphicsToIvyBridge,
+ nsIGfxInfo::FEATURE_GL_SWIZZLE,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_INTELHD4000_NO_SWIZZLE");
+ // We block texture swizzling everwhere on mac because it's broken in some configurations
+ // and we want to support GPU switching.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::All, nsIGfxInfo::FEATURE_GL_SWIZZLE,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_MAC_GPU_SWITCHING_NO_SWIZZLE");
+
+ // Older generation Intel devices do not perform well with WebRender.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::IntelWebRenderBlocked, nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_INTEL_GEN5_OR_OLDER");
+
+ // Intel HD3000 disabled due to bug 1661505
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::IntelSandyBridge, nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_INTEL_MAC_HD3000_NO_WEBRENDER");
+
+ // wgpu doesn't safely support OOB behavior on Metal yet.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(OperatingSystem::OSX, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBGPU, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_WGPU_NO_METAL_BOUNDS_CHECKS");
+ }
+ return *sDriverInfo;
+}
+
+OperatingSystem GfxInfo::GetOperatingSystem() { return OSXVersionToOperatingSystem(mOSXVersion); }
+
+nsresult GfxInfo::GetFeatureStatusImpl(int32_t aFeature, int32_t* aStatus,
+ nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo,
+ nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */) {
+ NS_ENSURE_ARG_POINTER(aStatus);
+ aSuggestedDriverVersion.SetIsVoid(true);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ OperatingSystem os = OSXVersionToOperatingSystem(mOSXVersion);
+ if (aOS) *aOS = os;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ // Don't evaluate special cases when we're evaluating the downloaded blocklist.
+ if (!aDriverInfo.Length()) {
+ if (aFeature == nsIGfxInfo::FEATURE_CANVAS2D_ACCELERATION) {
+ // See bug 1249659
+ switch (os) {
+ case OperatingSystem::OSX10_5:
+ case OperatingSystem::OSX10_6:
+ case OperatingSystem::OSX10_7:
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_OS_VERSION;
+ aFailureId = "FEATURE_FAILURE_CANVAS_OSX_VERSION";
+ break;
+ default:
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ break;
+ }
+ return NS_OK;
+ } else if (aFeature == nsIGfxInfo::FEATURE_WEBRENDER &&
+ nsCocoaFeatures::ProcessIsRosettaTranslated()) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_UNQUALIFIED_WEBRENDER_MAC_ROSETTA";
+ return NS_OK;
+ }
+ }
+
+ return GfxInfoBase::GetFeatureStatusImpl(aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo,
+ aFailureId, &os);
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+
+/* void spoofVendorID (in DOMString aVendorID); */
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ mAdapterVendorID[0] = aVendorID;
+ return NS_OK;
+}
+
+/* void spoofDeviceID (in unsigned long aDeviceID); */
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ mAdapterDeviceID[0] = aDeviceID;
+ return NS_OK;
+}
+
+/* void spoofDriverVersion (in DOMString aDriverVersion); */
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ mDriverVersion[0] = aDriverVersion;
+ return NS_OK;
+}
+
+/* void spoofOSVersion (in unsigned long aVersion); */
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ mOSXVersion = aVersion;
+ return NS_OK;
+}
+
+#endif
diff --git a/widget/cocoa/MOZIconHelper.h b/widget/cocoa/MOZIconHelper.h
new file mode 100644
index 0000000000..57783fc674
--- /dev/null
+++ b/widget/cocoa/MOZIconHelper.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef MOZIconHelper_h
+#define MOZIconHelper_h
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsRect.h"
+
+class imgIContainer;
+class nsPresContext;
+
+namespace mozilla {
+class ComputedStyle;
+}
+
+@interface MOZIconHelper : NSObject
+
+// Returns an autoreleased empty NSImage.
++ (NSImage*)placeholderIconWithSize:(NSSize)aSize;
+
+// Returns an autoreleased NSImage.
++ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
+ withSize:(NSSize)aSize
+ presContext:(const nsPresContext*)aPresContext
+ computedStyle:(const mozilla::ComputedStyle*)aComputedStyle
+ scaleFactor:(CGFloat)aScaleFactor;
+
+@end
+
+#endif // MOZIconHelper_h
diff --git a/widget/cocoa/MOZIconHelper.mm b/widget/cocoa/MOZIconHelper.mm
new file mode 100644
index 0000000000..2af89af2ed
--- /dev/null
+++ b/widget/cocoa/MOZIconHelper.mm
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Creates icons for display in native menu items on macOS.
+ */
+
+#include "MOZIconHelper.h"
+
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+
+@implementation MOZIconHelper
+
+// Returns an autoreleased empty NSImage.
++ (NSImage*)placeholderIconWithSize:(NSSize)aSize {
+ return [[[NSImage alloc] initWithSize:aSize] autorelease];
+}
+
+// Returns an autoreleased NSImage.
++ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
+ withSize:(NSSize)aSize
+ presContext:(const nsPresContext*)aPresContext
+ computedStyle:(const mozilla::ComputedStyle*)aComputedStyle
+ scaleFactor:(CGFloat)aScaleFactor {
+ bool isEntirelyBlack = false;
+ NSImage* retainedImage = nil;
+ nsresult rv;
+ if (aScaleFactor != 0.0f) {
+ rv = nsCocoaUtils::CreateNSImageFromImageContainer(aImage, imgIContainer::FRAME_CURRENT,
+ aPresContext, aComputedStyle, &retainedImage,
+ aScaleFactor, &isEntirelyBlack);
+ } else {
+ rv = nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
+ aImage, imgIContainer::FRAME_CURRENT, aPresContext, aComputedStyle, &retainedImage,
+ &isEntirelyBlack);
+ }
+
+ NSImage* image = [retainedImage autorelease];
+
+ if (NS_FAILED(rv) || !image) {
+ return nil;
+ }
+
+ int32_t origWidth = 0, origHeight = 0;
+ aImage->GetWidth(&origWidth);
+ aImage->GetHeight(&origHeight);
+
+ // If all the color channels in the image are black, treat the image as a
+ // template. This will cause macOS to use the image's alpha channel as a mask
+ // and it will fill it with a color that looks good in the context that it's
+ // used in. For example, for regular menu items, the image will be black, but
+ // when the menu item is hovered (and its background is blue), it will be
+ // filled with white.
+ [image setTemplate:isEntirelyBlack];
+
+ [image setSize:aSize];
+
+ return image;
+}
+
+@end
diff --git a/widget/cocoa/MOZMenuOpeningCoordinator.h b/widget/cocoa/MOZMenuOpeningCoordinator.h
new file mode 100644
index 0000000000..55dc2b2d59
--- /dev/null
+++ b/widget/cocoa/MOZMenuOpeningCoordinator.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef MOZMenuOpeningCoordinator_h
+#define MOZMenuOpeningCoordinator_h
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+class Runnable;
+}
+
+/*
+ * MOZMenuOpeningCoordinator is a workaround for the fact that opening an NSMenu creates a nested
+ * event loop. This event loop is only exited after the menu is closed. The caller of
+ * NativeMenuMac::ShowAsContextMenu does not expect ShowAsContextMenu to create a nested event loop,
+ * so we need to make sure to open the NSMenu asynchronously.
+ */
+
+@interface MOZMenuOpeningCoordinator : NSObject
+
++ (instancetype)sharedInstance;
+
+// Queue aMenu for opening.
+// The menu will open from a new event loop tick so that its nested event loop does not block the
+// caller. If another menu's nested event loop is currently on the stack, we wait for that nested
+// event loop to unwind before opening aMenu. Returns a handle that can be passed to
+// cancelAsynchronousOpening:. Can only be called on the main thread.
+- (NSInteger)asynchronouslyOpenMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu;
+
+// If the menu opening request for aHandle hasn't been processed yet, cancel it.
+// Can only be called on the main thread.
+- (void)cancelAsynchronousOpening:(NSInteger)aHandle;
+
+// This field is a terrible workaround for a gnarly problem.
+// It should be set to YES by the caller of -[NSMenu cancelTracking(WithoutAnimation)].
+// This field gets checked by the native event loop code in nsAppShell.mm to avoid calling
+// -[NSApplication nextEventMatchingMask:...] between the call to cancelTracking and the point at
+// which the menu has finished closing and unwound from its tracking event loop, because such calls
+// can interfere with menu closing and get us stuck in the menu event loop forever.
+@property(class) BOOL needToUnwindForMenuClosing;
+
+@end
+
+#endif // MOZMenuOpeningCoordinator_h
diff --git a/widget/cocoa/MOZMenuOpeningCoordinator.mm b/widget/cocoa/MOZMenuOpeningCoordinator.mm
new file mode 100644
index 0000000000..3ac4d8385f
--- /dev/null
+++ b/widget/cocoa/MOZMenuOpeningCoordinator.mm
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Makes sure that the nested event loop for NSMenu tracking is situated as low
+ * on the stack as possible, and that two NSMenu event loops are never nested.
+ */
+
+#include "MOZMenuOpeningCoordinator.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "nsMenuX.h"
+#include "nsObjCExceptions.h"
+#include "SDKDeclarations.h"
+
+static BOOL sNeedToUnwindForMenuClosing = NO;
+
+@interface MOZMenuOpeningInfo : NSObject
+@property NSInteger handle;
+@property(retain) NSMenu* menu;
+@property NSPoint position;
+@property(retain) NSView* view;
+@property(retain) NSAppearance* appearance;
+@property BOOL isContextMenu;
+@end
+
+@implementation MOZMenuOpeningInfo
+@end
+
+@implementation MOZMenuOpeningCoordinator {
+ // non-nil between asynchronouslyOpenMenu:atScreenPosition:forView: and the
+ // time at at which it is unqueued in _runMenu.
+ MOZMenuOpeningInfo* mPendingOpening; // strong
+
+ // An incrementing counter
+ NSInteger mLastHandle;
+
+ // YES while _runMenu is on the stack
+ BOOL mRunMenuIsOnTheStack;
+}
+
++ (instancetype)sharedInstance {
+ static MOZMenuOpeningCoordinator* sInstance = nil;
+ if (!sInstance) {
+ sInstance = [[MOZMenuOpeningCoordinator alloc] init];
+ mozilla::RunOnShutdown([&]() {
+ [sInstance release];
+ sInstance = nil;
+ });
+ }
+ return sInstance;
+}
+
+- (void)dealloc {
+ MOZ_RELEASE_ASSERT(!mPendingOpening, "should be empty at shutdown");
+ [super dealloc];
+}
+
+- (NSInteger)asynchronouslyOpenMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu {
+ MOZ_RELEASE_ASSERT(!mPendingOpening,
+ "A menu is already waiting to open. Before opening the next one, either wait "
+ "for this one to open or cancel the request.");
+
+ NSInteger handle = ++mLastHandle;
+
+ MOZMenuOpeningInfo* info = [[MOZMenuOpeningInfo alloc] init];
+ info.handle = handle;
+ info.menu = aMenu;
+ info.position = aPosition;
+ info.view = aView;
+ info.appearance = aAppearance;
+ info.isContextMenu = aIsContextMenu;
+ mPendingOpening = [info retain];
+ [info release];
+
+ if (!mRunMenuIsOnTheStack) {
+ // Call _runMenu from the event loop, so that it doesn't block this call.
+ [self performSelector:@selector(_runMenu) withObject:nil afterDelay:0.0];
+ }
+
+ return handle;
+}
+
+- (void)_runMenu {
+ MOZ_RELEASE_ASSERT(!mRunMenuIsOnTheStack);
+
+ mRunMenuIsOnTheStack = YES;
+
+ while (mPendingOpening) {
+ MOZMenuOpeningInfo* info = [mPendingOpening retain];
+ [mPendingOpening release];
+ mPendingOpening = nil;
+
+ @try {
+ [self _openMenu:info.menu
+ atScreenPosition:info.position
+ forView:info.view
+ withAppearance:info.appearance
+ asContextMenu:info.isContextMenu];
+ } @catch (NSException* exception) {
+ nsObjCExceptionLog(exception);
+ }
+
+ [info release];
+
+ // We have exited _openMenu's nested event loop.
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = NO;
+ }
+
+ mRunMenuIsOnTheStack = NO;
+}
+
+- (void)cancelAsynchronousOpening:(NSInteger)aHandle {
+ if (mPendingOpening && mPendingOpening.handle == aHandle) {
+ [mPendingOpening release];
+ mPendingOpening = nil;
+ }
+}
+
+- (void)_openMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu {
+ // There are multiple ways to display an NSMenu as a context menu.
+ //
+ // 1. We can return the NSMenu from -[ChildView menuForEvent:] and the NSView will open it for
+ // us.
+ // 2. We can call +[NSMenu popUpContextMenu:withEvent:forView:] inside a mouseDown handler with a
+ // real mouse down event.
+ // 3. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at a later time, with a real
+ // mouse event that we stored earlier.
+ // 4. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at any time, with a synthetic
+ // mouse event that we create just for that purpose.
+ // 5. We can call -[NSMenu popUpMenuPositioningItem:atLocation:inView:] and it just takes a
+ // position, not an event.
+ //
+ // 1-4 look the same, 5 looks different: 5 is made for use with NSPopUpButton, where the selected
+ // item needs to be shown at a specific position. If a tall menu is opened with a position close
+ // to the bottom edge of the screen, 5 results in a cropped menu with scroll arrows, even if the
+ // entire menu would fit on the screen, due to the positioning constraint.
+ // 1-2 only work if the menu contents are known synchronously during the call to menuForEvent or
+ // during the mouseDown event handler.
+ // NativeMenuMac::ShowAsContextMenu can be called at any time. It could be called during a
+ // menuForEvent call (during a "contextmenu" event handler), or during a mouseDown handler, or at
+ // a later time.
+ // The code below uses option 4 as the preferred option for context menus because it's the
+ // simplest: It works in all scenarios and it doesn't have the drawbacks of option 5. For popups
+ // that aren't context menus and that should be positioned as close as possible to the given
+ // screen position, we use option 5.
+
+ if (aAppearance) {
+#if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+#else
+ if (@available(macOS 11.0, *)) {
+#endif
+ // By default, NSMenu inherits its appearance from the opening NSEvent's
+ // window. If CSS has overridden it, on Big Sur + we can respect it with
+ // -[NSMenu setAppearance].
+ aMenu.appearance = aAppearance;
+ }
+ }
+
+ if (aView) {
+ NSWindow* window = aView.window;
+ NSPoint locationInWindow = nsCocoaUtils::ConvertPointFromScreen(window, aPosition);
+ if (aIsContextMenu) {
+ // Create a synthetic event at the right location and open the menu [option 4].
+ NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
+ location:locationInWindow
+ modifierFlags:0
+ timestamp:NSProcessInfo.processInfo.systemUptime
+ windowNumber:window.windowNumber
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:0.0f];
+ [NSMenu popUpContextMenu:aMenu withEvent:event forView:aView];
+ } else {
+ // For popups which are not context menus, we open the menu using [option
+ // 5]. We pass `nil` to indicate that we're positioning the top left
+ // corner of the menu. This path is used for anchored menupopups, so we
+ // prefer option 5 over option 4 so that the menu doesn't get flipped if
+ // space is tight.
+ NSPoint locationInView = [aView convertPoint:locationInWindow fromView:nil];
+ [aMenu popUpMenuPositioningItem:nil atLocation:locationInView inView:aView];
+ }
+ } else {
+ // Open the menu using popUpMenuPositioningItem:atLocation:inView: [option 5].
+ // This is not preferred, because it positions the menu differently from how a native context
+ // menu would be positioned; it enforces aPosition for the top left corner even if this
+ // means that the menu will be displayed in a clipped fashion with scroll arrows.
+ [aMenu popUpMenuPositioningItem:nil atLocation:aPosition inView:nil];
+ }
+}
+
++ (void)setNeedToUnwindForMenuClosing:(BOOL)aValue {
+ sNeedToUnwindForMenuClosing = aValue;
+}
+
++ (BOOL)needToUnwindForMenuClosing {
+ return sNeedToUnwindForMenuClosing;
+}
+
+@end
diff --git a/widget/cocoa/MacThemeGeometryType.h b/widget/cocoa/MacThemeGeometryType.h
new file mode 100644
index 0000000000..559e21d792
--- /dev/null
+++ b/widget/cocoa/MacThemeGeometryType.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+#ifndef mozilla_widget_ThemeGeometryType
+#define mozilla_widget_ThemeGeometryType
+
+enum MacThemeGeometryType {
+ eThemeGeometryTypeTitlebar = 1,
+ eThemeGeometryTypeToolbar,
+ eThemeGeometryTypeToolbox,
+ eThemeGeometryTypeWindowButtons,
+ eThemeGeometryTypeMenu,
+ eThemeGeometryTypeHighlightedMenuItem,
+ eThemeGeometryTypeTooltip,
+ eThemeGeometryTypeSourceList,
+ eThemeGeometryTypeSourceListSelection,
+ eThemeGeometryTypeActiveSourceListSelection
+};
+
+#endif
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMac.h b/widget/cocoa/MediaHardwareKeysEventSourceMac.h
new file mode 100644
index 0000000000..da08b8108d
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMac.h
@@ -0,0 +1,47 @@
+/* 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/. */
+
+#ifndef WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMAC_H_
+#define WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMAC_H_
+
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreFoundation/CoreFoundation.h>
+
+#include "mozilla/dom/MediaControlKeySource.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace widget {
+
+class MediaHardwareKeysEventSourceMac final
+ : public mozilla::dom::MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaHardwareKeysEventSourceMac, override)
+ MediaHardwareKeysEventSourceMac() = default;
+
+ static CGEventRef EventTapCallback(CGEventTapProxy proxy, CGEventType type,
+ CGEventRef event, void* refcon);
+
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+
+ // Currently we don't support showing supported keys on the touch bar.
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaHardwareKeysEventSourceMac() = default;
+
+ bool StartEventTap();
+ void StopEventTap();
+
+ // They are used to intercept mac hardware media keys.
+ CFMachPortRef mEventTap = nullptr;
+ CFRunLoopSourceRef mEventTapSource = nullptr;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMac.mm b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm
new file mode 100644
index 0000000000..669410018b
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm
@@ -0,0 +1,183 @@
+/* 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/. */
+
+#include "MediaHardwareKeysEventSourceMac.h"
+
+#import <AppKit/AppKit.h>
+#import <AppKit/NSEvent.h>
+#import <IOKit/hidsystem/ev_keymap.h>
+
+#include "mozilla/dom/MediaControlUtils.h"
+
+using namespace mozilla::dom;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__))
+
+// This macro is used in static callback function, where we have to send `this`
+// explicitly.
+#define LOG2(msg, this, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__))
+
+static const char* ToMediaControlKeyStr(int aKeyCode) {
+ switch (aKeyCode) {
+ case NX_KEYTYPE_PLAY:
+ return "Play";
+ case NX_KEYTYPE_NEXT:
+ return "Next";
+ case NX_KEYTYPE_PREVIOUS:
+ return "Previous";
+ case NX_KEYTYPE_FAST:
+ return "Fast";
+ case NX_KEYTYPE_REWIND:
+ return "Rewind";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid key code.");
+ return "UNKNOWN";
+ }
+}
+
+// The media keys subtype. No official docs found, but widely known.
+// http://lists.apple.com/archives/cocoa-dev/2007/Aug/msg00499.html
+const int kSystemDefinedEventMediaKeysSubtype = 8;
+
+static bool IsSupportedKeyCode(int aKeyCode) {
+ return aKeyCode == NX_KEYTYPE_PLAY || aKeyCode == NX_KEYTYPE_NEXT ||
+ aKeyCode == NX_KEYTYPE_FAST || aKeyCode == NX_KEYTYPE_PREVIOUS ||
+ aKeyCode == NX_KEYTYPE_REWIND;
+}
+
+static MediaControlKey ToMediaControlKey(int aKeyCode) {
+ MOZ_ASSERT(IsSupportedKeyCode(aKeyCode));
+ switch (aKeyCode) {
+ case NX_KEYTYPE_NEXT:
+ case NX_KEYTYPE_FAST:
+ return MediaControlKey::Nexttrack;
+ case NX_KEYTYPE_PREVIOUS:
+ case NX_KEYTYPE_REWIND:
+ return MediaControlKey::Previoustrack;
+ default:
+ MOZ_ASSERT(aKeyCode == NX_KEYTYPE_PLAY);
+ return MediaControlKey::Playpause;
+ }
+}
+
+namespace mozilla {
+namespace widget {
+
+bool MediaHardwareKeysEventSourceMac::IsOpened() const { return mEventTap && mEventTapSource; }
+
+bool MediaHardwareKeysEventSourceMac::Open() {
+ LOG("Open MediaHardwareKeysEventSourceMac");
+ return StartEventTap();
+}
+
+void MediaHardwareKeysEventSourceMac::Close() {
+ LOG("Close MediaHardwareKeysEventSourceMac");
+ StopEventTap();
+ MediaControlKeySource::Close();
+}
+
+bool MediaHardwareKeysEventSourceMac::StartEventTap() {
+ LOG("StartEventTap");
+ MOZ_ASSERT(!mEventTap);
+ MOZ_ASSERT(!mEventTapSource);
+
+ // Add an event tap to intercept the system defined media key events.
+ mEventTap =
+ CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly,
+ CGEventMaskBit(NX_SYSDEFINED), EventTapCallback, this);
+ if (!mEventTap) {
+ LOG("Fail to create event tap");
+ return false;
+ }
+
+ mEventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mEventTap, 0);
+ if (!mEventTapSource) {
+ LOG("Fail to create an event tap source.");
+ return false;
+ }
+
+ LOG("Add an event tap source to current loop");
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), mEventTapSource, kCFRunLoopCommonModes);
+ return true;
+}
+
+void MediaHardwareKeysEventSourceMac::StopEventTap() {
+ LOG("StopEventTapIfNecessary");
+ if (mEventTap) {
+ CFMachPortInvalidate(mEventTap);
+ mEventTap = nullptr;
+ }
+ if (mEventTapSource) {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mEventTapSource, kCFRunLoopCommonModes);
+ CFRelease(mEventTapSource);
+ mEventTapSource = nullptr;
+ }
+}
+
+CGEventRef MediaHardwareKeysEventSourceMac::EventTapCallback(CGEventTapProxy proxy,
+ CGEventType type, CGEventRef event,
+ void* refcon) {
+ // Re-enable event tap when receiving disabled events.
+ MediaHardwareKeysEventSourceMac* source = static_cast<MediaHardwareKeysEventSourceMac*>(refcon);
+ if (type == kCGEventTapDisabledByUserInput || type == kCGEventTapDisabledByTimeout) {
+ MOZ_ASSERT(source->mEventTap);
+ CGEventTapEnable(source->mEventTap, true);
+ return event;
+ }
+
+ NSEvent* nsEvent = [NSEvent eventWithCGEvent:event];
+ if (nsEvent == nil) {
+ return event;
+ }
+
+ // Ignore not system defined media keys event.
+ if ([nsEvent type] != NSEventTypeSystemDefined ||
+ [nsEvent subtype] != kSystemDefinedEventMediaKeysSubtype) {
+ return event;
+ }
+
+ // Ignore media keys that aren't previous, next and play/pause.
+ // Magical constants are from http://weblog.rogueamoeba.com/2007/09/29/
+ // - keyCode = (([event data1] & 0xFFFF0000) >> 16)
+ // - keyFlags = ([event data1] & 0x0000FFFF)
+ // - keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
+ // - keyRepeat = (keyFlags & 0x1);
+ const NSInteger data1 = [nsEvent data1];
+ int keyCode = (data1 & 0xFFFF0000) >> 16;
+ if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_NEXT && keyCode != NX_KEYTYPE_PREVIOUS &&
+ keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND) {
+ return event;
+ }
+
+ // Ignore non-key pressed event (eg. key released).
+ int keyFlags = data1 & 0x0000FFFF;
+ bool isKeyPressed = ((keyFlags & 0xFF00) >> 8) == 0xA;
+ if (!isKeyPressed) {
+ return event;
+ }
+
+ // There is no listener waiting to process event.
+ if (source->mListeners.IsEmpty()) {
+ return event;
+ }
+
+ if (!IsSupportedKeyCode(keyCode)) {
+ return event;
+ }
+
+ LOG2("Get media key %s", source, ToMediaControlKeyStr(keyCode));
+ for (auto iter = source->mListeners.begin(); iter != source->mListeners.end(); ++iter) {
+ (*iter)->OnActionPerformed(MediaControlAction(ToMediaControlKey(keyCode)));
+ }
+ return event;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h
new file mode 100644
index 0000000000..04a3aeba48
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h
@@ -0,0 +1,60 @@
+/* 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/. */
+
+#ifndef WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
+#define WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
+
+#include "mozilla/dom/MediaControlKeySource.h"
+
+#ifdef __OBJC__
+@class MPRemoteCommandEvent;
+#else
+typedef struct objc_object MPRemoteCommandEvent;
+#endif
+enum MPRemoteCommandHandlerStatus : long;
+
+namespace mozilla {
+namespace widget {
+
+typedef MPRemoteCommandHandlerStatus (^MediaCenterEventHandler)(MPRemoteCommandEvent* event);
+
+class MediaHardwareKeysEventSourceMacMediaCenter final
+ : public mozilla::dom::MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaHardwareKeysEventSourceMacMediaCenter, override)
+ MediaHardwareKeysEventSourceMacMediaCenter();
+
+ MediaCenterEventHandler CreatePlayPauseHandler();
+ MediaCenterEventHandler CreateNextTrackHandler();
+ MediaCenterEventHandler CreatePreviousTrackHandler();
+ MediaCenterEventHandler CreatePlayHandler();
+ MediaCenterEventHandler CreatePauseHandler();
+
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+ void SetPlaybackState(dom::MediaSessionPlaybackState aState) override;
+ void SetMediaMetadata(const dom::MediaMetadataBase& aMetadata) override;
+ // Currently we don't support showing supported keys on the touch bar.
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaHardwareKeysEventSourceMacMediaCenter();
+ void BeginListeningForEvents();
+ void EndListeningForEvents();
+ void HandleEvent(dom::MediaControlKey aKey);
+
+ bool mOpened = false;
+
+ MediaCenterEventHandler mPlayPauseHandler;
+ MediaCenterEventHandler mNextTrackHandler;
+ MediaCenterEventHandler mPreviousTrackHandler;
+ MediaCenterEventHandler mPauseHandler;
+ MediaCenterEventHandler mPlayHandler;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm
new file mode 100644
index 0000000000..9637c11d81
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm
@@ -0,0 +1,172 @@
+/* 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 <MediaPlayer/MediaPlayer.h>
+
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+
+#include "mozilla/dom/MediaControlUtils.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::dom;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMacMediaCenter=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+MediaCenterEventHandler MediaHardwareKeysEventSourceMacMediaCenter::CreatePlayPauseHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = center.playbackState == MPNowPlayingPlaybackStatePlaying
+ ? MPNowPlayingPlaybackStatePaused
+ : MPNowPlayingPlaybackStatePlaying;
+ HandleEvent(MediaControlKey::Playpause);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler MediaHardwareKeysEventSourceMacMediaCenter::CreateNextTrackHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ HandleEvent(MediaControlKey::Nexttrack);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler MediaHardwareKeysEventSourceMacMediaCenter::CreatePreviousTrackHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ HandleEvent(MediaControlKey::Previoustrack);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler MediaHardwareKeysEventSourceMacMediaCenter::CreatePlayHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (center.playbackState != MPNowPlayingPlaybackStatePlaying) {
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ }
+ HandleEvent(MediaControlKey::Play);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler MediaHardwareKeysEventSourceMacMediaCenter::CreatePauseHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (center.playbackState != MPNowPlayingPlaybackStatePaused) {
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ }
+ HandleEvent(MediaControlKey::Pause);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaHardwareKeysEventSourceMacMediaCenter::MediaHardwareKeysEventSourceMacMediaCenter() {
+ mPlayPauseHandler = CreatePlayPauseHandler();
+ mNextTrackHandler = CreateNextTrackHandler();
+ mPreviousTrackHandler = CreatePreviousTrackHandler();
+ mPlayHandler = CreatePlayHandler();
+ mPauseHandler = CreatePauseHandler();
+ LOG("Create MediaHardwareKeysEventSourceMacMediaCenter");
+}
+
+MediaHardwareKeysEventSourceMacMediaCenter::~MediaHardwareKeysEventSourceMacMediaCenter() {
+ LOG("Destroy MediaHardwareKeysEventSourceMacMediaCenter");
+ EndListeningForEvents();
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStateStopped;
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::BeginListeningForEvents() {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ MPRemoteCommandCenter* commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
+ commandCenter.togglePlayPauseCommand.enabled = true;
+ [commandCenter.togglePlayPauseCommand addTargetWithHandler:mPlayPauseHandler];
+ commandCenter.nextTrackCommand.enabled = true;
+ [commandCenter.nextTrackCommand addTargetWithHandler:mNextTrackHandler];
+ commandCenter.previousTrackCommand.enabled = true;
+ [commandCenter.previousTrackCommand addTargetWithHandler:mPreviousTrackHandler];
+ commandCenter.playCommand.enabled = true;
+ [commandCenter.playCommand addTargetWithHandler:mPlayHandler];
+ commandCenter.pauseCommand.enabled = true;
+ [commandCenter.pauseCommand addTargetWithHandler:mPauseHandler];
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::EndListeningForEvents() {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ center.nowPlayingInfo = nil;
+ MPRemoteCommandCenter* commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
+ commandCenter.togglePlayPauseCommand.enabled = false;
+ [commandCenter.togglePlayPauseCommand removeTarget:nil];
+ commandCenter.nextTrackCommand.enabled = false;
+ [commandCenter.nextTrackCommand removeTarget:nil];
+ commandCenter.previousTrackCommand.enabled = false;
+ [commandCenter.previousTrackCommand removeTarget:nil];
+ commandCenter.playCommand.enabled = false;
+ [commandCenter.playCommand removeTarget:nil];
+ commandCenter.pauseCommand.enabled = false;
+ [commandCenter.pauseCommand removeTarget:nil];
+}
+
+bool MediaHardwareKeysEventSourceMacMediaCenter::Open() {
+ LOG("Open MediaHardwareKeysEventSourceMacMediaCenter");
+ mOpened = true;
+ BeginListeningForEvents();
+ return true;
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::Close() {
+ LOG("Close MediaHardwareKeysEventSourceMacMediaCenter");
+ SetPlaybackState(dom::MediaSessionPlaybackState::None);
+ EndListeningForEvents();
+ mOpened = false;
+ MediaControlKeySource::Close();
+}
+
+bool MediaHardwareKeysEventSourceMacMediaCenter::IsOpened() const { return mOpened; }
+
+void MediaHardwareKeysEventSourceMacMediaCenter::HandleEvent(MediaControlKey aEvent) {
+ for (auto iter = mListeners.begin(); iter != mListeners.end(); ++iter) {
+ (*iter)->OnActionPerformed(MediaControlAction(aEvent));
+ }
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::SetPlaybackState(
+ MediaSessionPlaybackState aState) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (aState == MediaSessionPlaybackState::Playing) {
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ } else if (aState == MediaSessionPlaybackState::Paused) {
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ } else if (aState == MediaSessionPlaybackState::None) {
+ center.playbackState = MPNowPlayingPlaybackStateStopped;
+ }
+ MediaControlKeySource::SetPlaybackState(aState);
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::SetMediaMetadata(
+ const dom::MediaMetadataBase& aMetadata) {
+ NSMutableDictionary* nowPlayingInfo = [NSMutableDictionary dictionary];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mTitle)
+ forKey:MPMediaItemPropertyTitle];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mArtist)
+ forKey:MPMediaItemPropertyArtist];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mAlbum)
+ forKey:MPMediaItemPropertyAlbumTitle];
+ // The procedure of updating `nowPlayingInfo` is actually an async operation
+ // from our testing, Apple's documentation doesn't mention that though. So be
+ // aware that checking `nowPlayingInfo` immedately after setting it might not
+ // yield the expected result.
+ [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/MediaKeysEventSourceFactory.cpp b/widget/cocoa/MediaKeysEventSourceFactory.cpp
new file mode 100644
index 0000000000..1a5ad93b4c
--- /dev/null
+++ b/widget/cocoa/MediaKeysEventSourceFactory.cpp
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#include "MediaKeysEventSourceFactory.h"
+
+#include "MediaHardwareKeysEventSourceMac.h"
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+#include "nsCocoaFeatures.h"
+
+namespace mozilla {
+namespace widget {
+
+mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
+ if (nsCocoaFeatures::IsAtLeastVersion(10, 12, 2)) {
+ return new MediaHardwareKeysEventSourceMacMediaCenter();
+ } else {
+ return new MediaHardwareKeysEventSourceMac();
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeKeyBindings.h b/widget/cocoa/NativeKeyBindings.h
new file mode 100644
index 0000000000..6ab885fd95
--- /dev/null
+++ b/widget/cocoa/NativeKeyBindings.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef NativeKeyBindings_h
+#define NativeKeyBindings_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsTHashMap.h"
+#include "nsIWidget.h"
+
+struct objc_selector;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
+namespace widget {
+
+typedef nsTHashMap<nsPtrHashKey<objc_selector>, Command>
+ SelectorCommandHashtable;
+
+class NativeKeyBindings final {
+ public:
+ static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType);
+ static void Shutdown();
+
+ /**
+ * GetEditCommandsForTests() returns commands performed in native widget
+ * in typical environment. I.e., this does NOT refer customized shortcut
+ * key mappings of the environment.
+ */
+ static void GetEditCommandsForTests(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ void Init(NativeKeyBindingsType aType);
+
+ void GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ private:
+ NativeKeyBindings();
+
+ void AppendEditCommandsForSelector(objc_selector* aSelector,
+ nsTArray<CommandInt>& aCommands) const;
+
+ void LogEditCommands(const nsTArray<CommandInt>& aCommands,
+ const char* aDescription) const;
+
+ SelectorCommandHashtable mSelectorToCommand;
+
+ static NativeKeyBindings* sInstanceForSingleLineEditor;
+ static NativeKeyBindings* sInstanceForMultiLineEditor;
+}; // NativeKeyBindings
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // NativeKeyBindings_h
diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.mm
new file mode 100644
index 0000000000..d3e5983259
--- /dev/null
+++ b/widget/cocoa/NativeKeyBindings.mm
@@ -0,0 +1,605 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "NativeKeyBindings.h"
+
+#include "nsTArray.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
+
+#import <Cocoa/Cocoa.h>
+#import <Carbon/Carbon.h>
+
+namespace mozilla {
+namespace widget {
+
+static LazyLogModule gNativeKeyBindingsLog("NativeKeyBindings");
+
+NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
+NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
+
+// static
+NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ if (!sInstanceForSingleLineEditor) {
+ sInstanceForSingleLineEditor = new NativeKeyBindings();
+ sInstanceForSingleLineEditor->Init(aType);
+ }
+ return sInstanceForSingleLineEditor;
+ case NativeKeyBindingsType::MultiLineEditor:
+ case NativeKeyBindingsType::RichTextEditor:
+ if (!sInstanceForMultiLineEditor) {
+ sInstanceForMultiLineEditor = new NativeKeyBindings();
+ sInstanceForMultiLineEditor->Init(aType);
+ }
+ return sInstanceForMultiLineEditor;
+ default:
+ MOZ_CRASH("Not implemented");
+ return nullptr;
+ }
+}
+
+// static
+void NativeKeyBindings::Shutdown() {
+ delete sInstanceForSingleLineEditor;
+ sInstanceForSingleLineEditor = nullptr;
+ delete sInstanceForMultiLineEditor;
+ sInstanceForMultiLineEditor = nullptr;
+}
+
+NativeKeyBindings::NativeKeyBindings() {}
+
+inline objc_selector* ToObjcSelectorPtr(SEL aSel) { return reinterpret_cast<objc_selector*>(aSel); }
+#define SEL_TO_COMMAND(aSel, aCommand) \
+ mSelectorToCommand.InsertOrUpdate(ToObjcSelectorPtr(@selector(aSel)), aCommand)
+
+void NativeKeyBindings::Init(NativeKeyBindingsType aType) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, ("%p NativeKeyBindings::Init", this));
+
+ // Many selectors have a one-to-one mapping to a Gecko command. Those mappings
+ // are registered in mSelectorToCommand.
+
+ // Selectors from NSResponder's "Responding to Action Messages" section and
+ // from NSText's "Action Methods for Editing" section
+
+ // TODO: Improves correctness of left / right meaning
+ // TODO: Add real paragraph motions
+
+ // SEL_TO_COMMAND(cancelOperation:, );
+ // SEL_TO_COMMAND(capitalizeWord:, );
+ // SEL_TO_COMMAND(centerSelectionInVisibleArea:, );
+ // SEL_TO_COMMAND(changeCaseOfLetter:, );
+ // SEL_TO_COMMAND(complete:, );
+ SEL_TO_COMMAND(copy:, Command::Copy);
+ // SEL_TO_COMMAND(copyFont:, );
+ // SEL_TO_COMMAND(copyRuler:, );
+ SEL_TO_COMMAND(cut:, Command::Cut);
+ SEL_TO_COMMAND(delete:, Command::Delete);
+ SEL_TO_COMMAND(deleteBackward:, Command::DeleteCharBackward);
+ // SEL_TO_COMMAND(deleteBackwardByDecomposingPreviousCharacter:, );
+ SEL_TO_COMMAND(deleteForward:, Command::DeleteCharForward);
+
+ // TODO: deleteTo* selectors are also supposed to add text to a kill buffer
+ SEL_TO_COMMAND(deleteToBeginningOfLine:, Command::DeleteToBeginningOfLine);
+ SEL_TO_COMMAND(deleteToBeginningOfParagraph:, Command::DeleteToBeginningOfLine);
+ SEL_TO_COMMAND(deleteToEndOfLine:, Command::DeleteToEndOfLine);
+ SEL_TO_COMMAND(deleteToEndOfParagraph:, Command::DeleteToEndOfLine);
+ // SEL_TO_COMMAND(deleteToMark:, );
+
+ SEL_TO_COMMAND(deleteWordBackward:, Command::DeleteWordBackward);
+ SEL_TO_COMMAND(deleteWordForward:, Command::DeleteWordForward);
+ // SEL_TO_COMMAND(indent:, );
+ // SEL_TO_COMMAND(insertBacktab:, );
+ // SEL_TO_COMMAND(insertContainerBreak:, );
+ // SEL_TO_COMMAND(insertLineBreak:, );
+ // SEL_TO_COMMAND(insertNewline:, );
+ // SEL_TO_COMMAND(insertNewlineIgnoringFieldEditor:, );
+ // SEL_TO_COMMAND(insertParagraphSeparator:, );
+ // SEL_TO_COMMAND(insertTab:, );
+ // SEL_TO_COMMAND(insertTabIgnoringFieldEditor:, );
+ // SEL_TO_COMMAND(insertDoubleQuoteIgnoringSubstitution:, );
+ // SEL_TO_COMMAND(insertSingleQuoteIgnoringSubstitution:, );
+ // SEL_TO_COMMAND(lowercaseWord:, );
+ SEL_TO_COMMAND(moveBackward:, Command::CharPrevious);
+ SEL_TO_COMMAND(moveBackwardAndModifySelection:, Command::SelectCharPrevious);
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(moveDown:, Command::EndLine);
+ } else {
+ SEL_TO_COMMAND(moveDown:, Command::LineNext);
+ }
+ SEL_TO_COMMAND(moveDownAndModifySelection:, Command::SelectLineNext);
+ SEL_TO_COMMAND(moveForward:, Command::CharNext);
+ SEL_TO_COMMAND(moveForwardAndModifySelection:, Command::SelectCharNext);
+ SEL_TO_COMMAND(moveLeft:, Command::CharPrevious);
+ SEL_TO_COMMAND(moveLeftAndModifySelection:, Command::SelectCharPrevious);
+ SEL_TO_COMMAND(moveParagraphBackwardAndModifySelection:, Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveParagraphForwardAndModifySelection:, Command::SelectEndLine);
+ SEL_TO_COMMAND(moveRight:, Command::CharNext);
+ SEL_TO_COMMAND(moveRightAndModifySelection:, Command::SelectCharNext);
+ SEL_TO_COMMAND(moveToBeginningOfDocument:, Command::MoveTop);
+ SEL_TO_COMMAND(moveToBeginningOfDocumentAndModifySelection:, Command::SelectTop);
+ SEL_TO_COMMAND(moveToBeginningOfLine:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfLineAndModifySelection:, Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfParagraph:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfParagraphAndModifySelection:, Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToEndOfDocument:, Command::MoveBottom);
+ SEL_TO_COMMAND(moveToEndOfDocumentAndModifySelection:, Command::SelectBottom);
+ SEL_TO_COMMAND(moveToEndOfLine:, Command::EndLine);
+ SEL_TO_COMMAND(moveToEndOfLineAndModifySelection:, Command::SelectEndLine);
+ SEL_TO_COMMAND(moveToEndOfParagraph:, Command::EndLine);
+ SEL_TO_COMMAND(moveToEndOfParagraphAndModifySelection:, Command::SelectEndLine);
+ SEL_TO_COMMAND(moveToLeftEndOfLine:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToLeftEndOfLineAndModifySelection:, Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToRightEndOfLine:, Command::EndLine);
+ SEL_TO_COMMAND(moveToRightEndOfLineAndModifySelection:, Command::SelectEndLine);
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(moveUp:, Command::BeginLine);
+ } else {
+ SEL_TO_COMMAND(moveUp:, Command::LinePrevious);
+ }
+ SEL_TO_COMMAND(moveUpAndModifySelection:, Command::SelectLinePrevious);
+ SEL_TO_COMMAND(moveWordBackward:, Command::WordPrevious);
+ SEL_TO_COMMAND(moveWordBackwardAndModifySelection:, Command::SelectWordPrevious);
+ SEL_TO_COMMAND(moveWordForward:, Command::WordNext);
+ SEL_TO_COMMAND(moveWordForwardAndModifySelection:, Command::SelectWordNext);
+ SEL_TO_COMMAND(moveWordLeft:, Command::WordPrevious);
+ SEL_TO_COMMAND(moveWordLeftAndModifySelection:, Command::SelectWordPrevious);
+ SEL_TO_COMMAND(moveWordRight:, Command::WordNext);
+ SEL_TO_COMMAND(moveWordRightAndModifySelection:, Command::SelectWordNext);
+ SEL_TO_COMMAND(pageDown:, Command::MovePageDown);
+ SEL_TO_COMMAND(pageDownAndModifySelection:, Command::SelectPageDown);
+ SEL_TO_COMMAND(pageUp:, Command::MovePageUp);
+ SEL_TO_COMMAND(pageUpAndModifySelection:, Command::SelectPageUp);
+ SEL_TO_COMMAND(paste:, Command::Paste);
+ // SEL_TO_COMMAND(pasteFont:, );
+ // SEL_TO_COMMAND(pasteRuler:, );
+ SEL_TO_COMMAND(scrollLineDown:, Command::ScrollLineDown);
+ SEL_TO_COMMAND(scrollLineUp:, Command::ScrollLineUp);
+ SEL_TO_COMMAND(scrollPageDown:, Command::ScrollPageDown);
+ SEL_TO_COMMAND(scrollPageUp:, Command::ScrollPageUp);
+ SEL_TO_COMMAND(scrollToBeginningOfDocument:, Command::ScrollTop);
+ SEL_TO_COMMAND(scrollToEndOfDocument:, Command::ScrollBottom);
+ SEL_TO_COMMAND(selectAll:, Command::SelectAll);
+ // selectLine: is complex, see KeyDown
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(selectParagraph:, Command::SelectAll);
+ }
+ // SEL_TO_COMMAND(selectToMark:, );
+ // selectWord: is complex, see KeyDown
+ // SEL_TO_COMMAND(setMark:, );
+ // SEL_TO_COMMAND(showContextHelp:, );
+ // SEL_TO_COMMAND(supplementalTargetForAction:sender:, );
+ // SEL_TO_COMMAND(swapWithMark:, );
+ // SEL_TO_COMMAND(transpose:, );
+ // SEL_TO_COMMAND(transposeWords:, );
+ // SEL_TO_COMMAND(uppercaseWord:, );
+ // SEL_TO_COMMAND(yank:, );
+}
+
+#undef SEL_TO_COMMAND
+
+void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
+ MOZ_ASSERT(aCommands.IsEmpty());
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, ("%p NativeKeyBindings::GetEditCommands", this));
+
+ // Recover the current event, which should always be the key down we are
+ // responding to.
+
+ NSEvent* cocoaEvent = reinterpret_cast<NSEvent*>(aEvent.mNativeKeyEvent);
+
+ if (!cocoaEvent || [cocoaEvent type] != NSEventTypeKeyDown) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, no Cocoa key down event", this));
+
+ return;
+ }
+
+ if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
+ aWritingMode.ref().IsVertical()) {
+ NSEvent* originalEvent = cocoaEvent;
+
+ // TODO: Use KeyNameIndex rather than legacy keyCode.
+ uint32_t remappedGeckoKeyCode = aEvent.GetRemappedKeyCode(aWritingMode.ref());
+ uint32_t remappedCocoaKeyCode = 0;
+ switch (remappedGeckoKeyCode) {
+ case NS_VK_UP:
+ remappedCocoaKeyCode = kVK_UpArrow;
+ break;
+ case NS_VK_DOWN:
+ remappedCocoaKeyCode = kVK_DownArrow;
+ break;
+ case NS_VK_LEFT:
+ remappedCocoaKeyCode = kVK_LeftArrow;
+ break;
+ case NS_VK_RIGHT:
+ remappedCocoaKeyCode = kVK_RightArrow;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+ return;
+ }
+ unichar ch = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(remappedGeckoKeyCode);
+ NSString* chars = [[[NSString alloc] initWithCharacters:&ch length:1] autorelease];
+ cocoaEvent = [NSEvent keyEventWithType:[originalEvent type]
+ location:[originalEvent locationInWindow]
+ modifierFlags:[originalEvent modifierFlags]
+ timestamp:[originalEvent timestamp]
+ windowNumber:[originalEvent windowNumber]
+ context:nil
+ characters:chars
+ charactersIgnoringModifiers:chars
+ isARepeat:[originalEvent isARepeat]
+ keyCode:remappedCocoaKeyCode];
+ }
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, interpreting", this));
+
+ AutoTArray<KeyBindingsCommand, 2> bindingCommands;
+ nsCocoaUtils::GetCommandsFromKeyEvent(cocoaEvent, bindingCommands);
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, bindingCommands=%zu", this,
+ bindingCommands.Length()));
+
+ for (uint32_t i = 0; i < bindingCommands.Length(); i++) {
+ SEL selector = bindingCommands[i].selector;
+
+ if (MOZ_LOG_TEST(gNativeKeyBindingsLog, LogLevel::Info)) {
+ NSString* selectorString = NSStringFromSelector(selector);
+ nsAutoString nsSelectorString;
+ nsCocoaUtils::GetStringForNSString(selectorString, nsSelectorString);
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, selector=%s", this,
+ NS_LossyConvertUTF16toASCII(nsSelectorString).get()));
+ }
+
+ AppendEditCommandsForSelector(ToObjcSelectorPtr(selector), aCommands);
+ }
+
+ LogEditCommands(aCommands, "NativeKeyBindings::GetEditCommands");
+}
+
+void NativeKeyBindings::AppendEditCommandsForSelector(objc_selector* aSelector,
+ nsTArray<CommandInt>& aCommands) const {
+ // Try to find a simple mapping in the hashtable
+ Command geckoCommand = Command::DoNothing;
+ if (mSelectorToCommand.Get(aSelector, &geckoCommand) && geckoCommand != Command::DoNothing) {
+ aCommands.AppendElement(static_cast<CommandInt>(geckoCommand));
+ } else if (aSelector == ToObjcSelectorPtr(@selector(selectLine:))) {
+ // This is functional, but Cocoa's version is direction-less in that
+ // selection direction is not determined until some future directed action
+ // is taken. See bug 282097, comment 79 for more details.
+ aCommands.AppendElement(static_cast<CommandInt>(Command::BeginLine));
+ aCommands.AppendElement(static_cast<CommandInt>(Command::SelectEndLine));
+ } else if (aSelector == ToObjcSelectorPtr(@selector(selectWord:))) {
+ // This is functional, but Cocoa's version is direction-less in that
+ // selection direction is not determined until some future directed action
+ // is taken. See bug 282097, comment 79 for more details.
+ aCommands.AppendElement(static_cast<CommandInt>(Command::WordPrevious));
+ aCommands.AppendElement(static_cast<CommandInt>(Command::SelectWordNext));
+ }
+}
+
+void NativeKeyBindings::LogEditCommands(const nsTArray<CommandInt>& aCommands,
+ const char* aDescription) const {
+ if (!MOZ_LOG_TEST(gNativeKeyBindingsLog, LogLevel::Info)) {
+ return;
+ }
+
+ if (aCommands.IsEmpty()) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, ("%p %s, no edit commands", this, aDescription));
+ return;
+ }
+
+ for (CommandInt commandInt : aCommands) {
+ Command geckoCommand = static_cast<Command>(commandInt);
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p %s, command=%s", this, aDescription,
+ WidgetKeyboardEvent::GetCommandStr(geckoCommand)));
+ }
+}
+
+// static
+void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
+
+ // The following mapping is checked on Big Sur. Some of them are defined in:
+ // https://support.apple.com/en-us/HT201236#text
+ NativeKeyBindings* instance = NativeKeyBindings::GetInstance(aType);
+ if (NS_WARN_IF(!instance)) {
+ return;
+ }
+ switch (aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
+ : aEvent.mKeyNameIndex) {
+ case KEY_NAME_INDEX_USE_STRING:
+ if (!aEvent.IsControl() || aEvent.IsAlt() || aEvent.IsMeta()) {
+ break;
+ }
+ switch (aEvent.PseudoCharCode()) {
+ case 'a':
+ case 'A':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToBeginningOfParagraph:))
+ : ToObjcSelectorPtr(@selector(moveToBeginningOfParagraphAndModifySelection:)),
+ aCommands);
+ break;
+ case 'b':
+ case 'B':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveBackward:))
+ : ToObjcSelectorPtr(@selector(moveBackwardAndModifySelection:)),
+ aCommands);
+ break;
+ case 'd':
+ case 'D':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteForward:)),
+ aCommands);
+ }
+ break;
+ case 'e':
+ case 'E':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToEndOfParagraph:))
+ : ToObjcSelectorPtr(@selector(moveToEndOfParagraphAndModifySelection:)),
+ aCommands);
+ break;
+ case 'f':
+ case 'F':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveForward:))
+ : ToObjcSelectorPtr(@selector(moveForwardAndModifySelection:)),
+ aCommands);
+ break;
+ case 'h':
+ case 'H':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteBackward:)),
+ aCommands);
+ }
+ break;
+ case 'k':
+ case 'K':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteToEndOfParagraph:)), aCommands);
+ }
+ break;
+ case 'n':
+ case 'N':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveDown:))
+ : ToObjcSelectorPtr(@selector(moveDownAndModifySelection:)),
+ aCommands);
+ break;
+ case 'p':
+ case 'P':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveUp:))
+ : ToObjcSelectorPtr(@selector(moveUpAndModifySelection:)),
+ aCommands);
+ break;
+ default:
+ break;
+ }
+ break;
+ case KEY_NAME_INDEX_Backspace:
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt() || aEvent.IsControl()) {
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteToBeginningOfLine:)), aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift and Control are ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteWordBackward:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ if (aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteBackwardByDecomposingPreviousCharacter:)),
+ aCommands);
+ }
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteBackward:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_Delete:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteWordForward:)),
+ aCommands);
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(deleteForward:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_PageDown:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(pageDown:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(scrollPageDown:))
+ : ToObjcSelectorPtr(@selector(pageDownAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_PageUp:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(pageUp:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(scrollPageUp:))
+ : ToObjcSelectorPtr(@selector(pageUpAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_Home:
+ if (aEvent.IsAlt() || aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(scrollToBeginningOfDocument:))
+ : ToObjcSelectorPtr(@selector(moveToBeginningOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_End:
+ if (aEvent.IsAlt() || aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(scrollToEndOfDocument:))
+ : ToObjcSelectorPtr(@selector(moveToEndOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowLeft:
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToLeftEndOfLine:))
+ : ToObjcSelectorPtr(@selector(moveToLeftEndOfLineAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveLeft:))
+ : ToObjcSelectorPtr(@selector(moveLeftAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowRight:
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToRightEndOfLine:))
+ : ToObjcSelectorPtr(@selector(moveToRightEndOfLineAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveRight:))
+ : ToObjcSelectorPtr(@selector(moveRightAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowUp:
+ if (aEvent.IsControl()) {
+ break;
+ }
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToBeginningOfDocument:))
+ : ToObjcSelectorPtr(@selector(moveToBegginingOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(moveBackward:)),
+ aCommands);
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveToBeginningOfParagraph:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveParagraphBackwardAndModifySelection:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveUp:))
+ : ToObjcSelectorPtr(@selector(moveUpAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowDown:
+ if (aEvent.IsControl()) {
+ break;
+ }
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToEndOfDocument:))
+ : ToObjcSelectorPtr(@selector(moveToEndOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(ToObjcSelectorPtr(@selector(moveForward:)),
+ aCommands);
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveToEndOfParagraph:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveParagraphForwardAndModifySelection:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveDown:))
+ : ToObjcSelectorPtr(@selector(moveDownAndModifySelection:)),
+ aCommands);
+ break;
+ default:
+ break;
+ }
+
+ instance->LogEditCommands(aCommands, "NativeKeyBindings::GetEditCommandsForTests");
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeMenuMac.h b/widget/cocoa/NativeMenuMac.h
new file mode 100644
index 0000000000..f2f5d20033
--- /dev/null
+++ b/widget/cocoa/NativeMenuMac.h
@@ -0,0 +1,93 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef NativeMenuMac_h
+#define NativeMenuMac_h
+
+#include "mozilla/widget/NativeMenu.h"
+
+#include "nsMenuItemIconX.h"
+#include "nsMenuX.h"
+
+class nsIContent;
+class nsMenuGroupOwnerX;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+namespace widget {
+
+class NativeMenuMac : public NativeMenu,
+ public nsMenuItemIconX::Listener,
+ public nsMenuX::Observer {
+ public:
+ explicit NativeMenuMac(dom::Element* aElement);
+
+ // NativeMenu
+ void ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
+ bool aIsContextMenu) override;
+ bool Close() override;
+ void ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, int16_t aButton,
+ ErrorResult& aRv) override;
+ void OpenSubmenu(dom::Element* aMenuElement) override;
+ void CloseSubmenu(dom::Element* aMenuElement) override;
+ RefPtr<dom::Element> Element() override;
+ void AddObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.AppendElement(aObserver);
+ }
+ void RemoveObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.RemoveElement(aObserver);
+ }
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuX::Observer
+ void OnMenuWillOpen(dom::Element* aPopupElement) override;
+ void OnMenuDidOpen(dom::Element* aPopupElement) override;
+ void OnMenuWillActivateItem(dom::Element* aPopupElement, dom::Element* aMenuItemElement) override;
+ void OnMenuClosed(dom::Element* aPopupElement) override;
+
+ NSMenu* NativeNSMenu() { return mMenu ? mMenu->NativeNSMenu() : nil; }
+ void MenuWillOpen();
+
+ // Returns whether a menu item was found at the specified path.
+ bool ActivateNativeMenuItemAt(const nsAString& aIndexString);
+
+ void ForceUpdateNativeMenuAt(const nsAString& aIndexString);
+ void Dump();
+
+ // If this menu is the menu of a system status bar item (NSStatusItem),
+ // let the menu know about the status item so that it can propagate
+ // any icon changes to the status item.
+ void SetContainerStatusBarItem(NSStatusItem* aItem);
+
+ protected:
+ virtual ~NativeMenuMac();
+
+ // Find the deepest nsMenuX which contains aElement, only descending into open
+ // menus.
+ // Returns nullptr if the element was not found or if the menus on the path
+ // were not all open.
+ RefPtr<nsMenuX> GetOpenMenuContainingElement(dom::Element* aElement);
+
+ RefPtr<dom::Element> mElement;
+ RefPtr<nsMenuGroupOwnerX> mMenuGroupOwner;
+ RefPtr<nsMenuX> mMenu;
+ nsTArray<NativeMenu::Observer*> mObservers;
+ NSStatusItem* mContainerStatusBarItem;
+
+ // Non-zero after a call to ShowAsContextMenu. Stores the handle from the
+ // MOZMenuOpeningCoordinator.
+ NSInteger mOpeningHandle = 0;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/cocoa/NativeMenuMac.mm b/widget/cocoa/NativeMenuMac.mm
new file mode 100644
index 0000000000..061d6c7ad9
--- /dev/null
+++ b/widget/cocoa/NativeMenuMac.mm
@@ -0,0 +1,394 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "NativeMenuMac.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+
+#include "MOZMenuOpeningCoordinator.h"
+#include "nsISupports.h"
+#include "nsGkAtoms.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsNativeThemeColors.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+#include "PresShell.h"
+#include "nsCocoaUtils.h"
+#include "nsIFrame.h"
+#include "nsPresContext.h"
+#include "nsDeviceContext.h"
+#include "nsCocoaFeatures.h"
+
+namespace mozilla {
+
+using dom::Element;
+
+namespace widget {
+
+NativeMenuMac::NativeMenuMac(dom::Element* aElement)
+ : mElement(aElement), mContainerStatusBarItem(nil) {
+ MOZ_RELEASE_ASSERT(aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup));
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, nullptr);
+ mMenu = MakeRefPtr<nsMenuX>(nullptr, mMenuGroupOwner, aElement);
+ mMenu->SetObserver(this);
+ mMenu->SetIconListener(this);
+ mMenu->SetupIcon();
+}
+
+NativeMenuMac::~NativeMenuMac() {
+ mMenu->DetachFromGroupOwnerRecursive();
+ mMenu->ClearObserver();
+ mMenu->ClearIconListener();
+}
+
+static void UpdateMenu(nsMenuX* aMenu) {
+ aMenu->MenuOpened();
+ aMenu->MenuClosed();
+
+ uint32_t itemCount = aMenu->GetItemCount();
+ for (uint32_t i = 0; i < itemCount; i++) {
+ nsMenuX::MenuChild menuObject = *aMenu->GetItemAt(i);
+ if (menuObject.is<RefPtr<nsMenuX>>()) {
+ UpdateMenu(menuObject.as<RefPtr<nsMenuX>>());
+ }
+ }
+}
+
+void NativeMenuMac::MenuWillOpen() {
+ // Force an update on the mMenu by faking an open/close on all of
+ // its submenus.
+ UpdateMenu(mMenu.get());
+}
+
+bool NativeMenuMac::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+
+ nsMenuUtilsX::CheckNativeMenuConsistency(menu);
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSMenuItem* item = nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
+
+ // We can't perform an action on an item with a submenu, that will raise
+ // an obj-c exception.
+ if (item && !item.hasSubmenu) {
+ NSMenu* parent = item.menu;
+ if (parent) {
+ // NSLog(@"Performing action for native menu item titled: %@\n",
+ // [[currentSubmenu itemAtIndex:targetIndex] title]);
+ mozilla::AutoRestore<bool> autoRestore(
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
+ [parent performActionForItemAtIndex:[parent indexOfItem:item]];
+ return true;
+ }
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray<NSString*>* indexes = [locationString componentsSeparatedByString:@"|"];
+ RefPtr<nsMenuX> currentMenu = mMenu.get();
+
+ // now find the correct submenu
+ unsigned int indexCount = indexes.count;
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ int targetIndex = [indexes objectAtIndex:i].intValue;
+ int visible = 0;
+ uint32_t length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ break;
+ }
+ }
+ }
+ }
+
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::IconUpdated() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mContainerStatusBarItem) {
+ NSImage* menuImage = mMenu->NativeNSMenuItem().image;
+ if (menuImage) {
+ [menuImage setTemplate:YES];
+ }
+ mContainerStatusBarItem.button.image = menuImage;
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
+ mContainerStatusBarItem = aItem;
+ IconUpdated();
+}
+
+void NativeMenuMac::Dump() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenu->Dump(0);
+ nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
+ if (aPopupElement == mElement) {
+ return;
+ }
+
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeSubMenuWillOpen(aPopupElement);
+ }
+}
+
+void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuOpened();
+ } else {
+ observer->OnNativeSubMenuDidOpen(aPopupElement);
+ }
+ }
+}
+
+void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
+ dom::Element* aMenuItemElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeMenuWillActivateItem(aMenuItemElement);
+ }
+}
+
+void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout this function in case
+ // one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuClosed();
+ } else {
+ observer->OnNativeSubMenuClosed(aPopupElement);
+ }
+ }
+}
+
+static NSView* NativeViewForFrame(nsIFrame* aFrame) {
+ nsIWidget* widget = aFrame->GetNearestWidget();
+ return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
+}
+
+static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
+ nsIFrame* f = aContent->GetPrimaryFrame();
+ if (!f) {
+ return nil;
+ }
+ return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
+}
+
+void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
+ bool aIsContextMenu) {
+ nsPresContext* pc = aClickedFrame->PresContext();
+ auto cssToDesktopScale =
+ pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
+ const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
+
+ mMenu->PopupShowingEventWasSentAndApprovedExternally();
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+ NSView* view = NativeViewForFrame(aClickedFrame);
+ NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
+ NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
+
+ // Let the MOZMenuOpeningCoordinator do the actual opening, so that this ShowAsContextMenu call
+ // does not spawn a nested event loop, which would be surprising to our callers.
+ mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance asynchronouslyOpenMenu:menu
+ atScreenPosition:locationOnScreen
+ forView:view
+ withAppearance:appearance
+ asContextMenu:aIsContextMenu];
+}
+
+bool NativeMenuMac::Close() {
+ if (mOpeningHandle) {
+ // In case the menu was trying to open, but this Close() call interrupted it, cancel opening.
+ [MOZMenuOpeningCoordinator.sharedInstance cancelAsynchronousOpening:mOpeningHandle];
+ }
+ return mMenu->Close();
+}
+
+RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(dom::Element* aElement) {
+ nsTArray<RefPtr<dom::Element>> submenuChain;
+ RefPtr<dom::Element> currentElement = aElement->GetParentElement();
+ while (currentElement && currentElement != mElement) {
+ if (currentElement->IsXULElement(nsGkAtoms::menu)) {
+ submenuChain.AppendElement(currentElement);
+ }
+ currentElement = currentElement->GetParentElement();
+ }
+ if (!currentElement) {
+ // aElement was not a descendent of mElement. Refuse to activate the item.
+ return nullptr;
+ }
+
+ // Traverse submenuChain from shallow to deep, to find the nsMenuX that contains aElement.
+ submenuChain.Reverse();
+ RefPtr<nsMenuX> menu = mMenu;
+ for (const auto& submenu : submenuChain) {
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
+ if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
+ // Couldn't find submenu.
+ return nullptr;
+ }
+ menu = menuChild->as<RefPtr<nsMenuX>>();
+ }
+
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ return menu;
+}
+
+static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
+ NSEventModifierFlags flags = 0;
+ if (aModifiers & MODIFIER_CONTROL) {
+ flags |= NSEventModifierFlagControl;
+ }
+ if (aModifiers & MODIFIER_ALT) {
+ flags |= NSEventModifierFlagOption;
+ }
+ if (aModifiers & MODIFIER_SHIFT) {
+ flags |= NSEventModifierFlagShift;
+ }
+ if (aModifiers & MODIFIER_META) {
+ flags |= NSEventModifierFlagCommand;
+ }
+ return flags;
+}
+
+void NativeMenuMac::ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, int16_t aButton,
+ ErrorResult& aRv) {
+ RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
+ if (!menu) {
+ aRv.ThrowInvalidStateError("Menu containing menu item is not open");
+ return;
+ }
+ Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
+ if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
+ aRv.ThrowInvalidStateError("Could not find the supplied menu item");
+ return;
+ }
+
+ RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
+ if (!item->IsVisible()) {
+ aRv.ThrowInvalidStateError("Menu item is not visible");
+ return;
+ }
+
+ NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
+
+ // First, initiate the closing of the NSMenu.
+ // This synchronously calls the menu delegate's menuDidClose handler. So menuDidClose is
+ // what runs first; this matches the order of events for user-initiated menu item activation.
+ // This call doesn't immediately hide the menu; the menu only hides once the stack unwinds
+ // from NSMenu's nested "tracking" event loop.
+ [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
+
+ // Next, call OnWillActivateItem. This also matches the order of calls that happen when a user
+ // activates a menu item in the real world: -[MenuDelegate menu:willActivateItem:] runs after
+ // menuDidClose.
+ menu->OnWillActivateItem(nativeItem);
+
+ // Finally, call ActivateItemAfterClosing. This also mimics the order in the real world:
+ // menuItemHit is called after menu:willActivateItem:.
+ menu->ActivateItemAfterClosing(std::move(item), ConvertModifierFlags(aModifiers), aButton);
+
+ // Tell our native event loop that it should not process any more work before
+ // unwinding the stack, so that we can get out of the menu's nested event loop
+ // as fast as possible. This was needed to fix spurious failures in tests, where
+ // a call to cancelTrackingWithoutAnimation was ignored if more native events were
+ // processed before the event loop was exited. As a result, the menu stayed open
+ // forever and the test never finished.
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+
+ [nativeItem release];
+}
+
+void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuOpened();
+ }
+ }
+}
+
+void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuClosed();
+ }
+ }
+}
+
+RefPtr<Element> NativeMenuMac::Element() { return mElement; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeMenuSupport.mm b/widget/cocoa/NativeMenuSupport.mm
new file mode 100644
index 0000000000..98f9fc045e
--- /dev/null
+++ b/widget/cocoa/NativeMenuSupport.mm
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "mozilla/widget/NativeMenuSupport.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "NativeMenuMac.h"
+#include "nsCocoaWindow.h"
+#include "nsMenuBarX.h"
+
+namespace mozilla::widget {
+
+void NativeMenuSupport::CreateNativeMenuBar(nsIWidget* aParent, dom::Element* aMenuBarElement) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Attempting to create native menu bar on wrong thread!");
+
+ // Create the menubar and give it to the parent window. The parent takes ownership.
+ static_cast<nsCocoaWindow*>(aParent)->SetMenuBar(MakeRefPtr<nsMenuBarX>(aMenuBarElement));
+}
+
+already_AddRefed<NativeMenu> NativeMenuSupport::CreateNativeContextMenu(dom::Element* aPopup) {
+ return MakeAndAddRef<NativeMenuMac>(aPopup);
+}
+
+bool NativeMenuSupport::ShouldUseNativeContextMenus() {
+ return StaticPrefs::widget_macos_native_context_menus();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/cocoa/OSXNotificationCenter.h b/widget/cocoa/OSXNotificationCenter.h
new file mode 100644
index 0000000000..9ff1ff62d8
--- /dev/null
+++ b/widget/cocoa/OSXNotificationCenter.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef OSXNotificationCenter_h
+#define OSXNotificationCenter_h
+
+#import <Foundation/Foundation.h>
+#include "nsIAlertsService.h"
+#include "nsTArray.h"
+#include "mozilla/RefPtr.h"
+
+// mozNotificationCenterDelegate is used to access the macOS notification
+// center. It is not related to the DesktopNotificationCenter object, which was
+// removed in bug 952453. While there are no direct references to this class
+// elsewhere, removing this will cause push notifications on macOS to stop
+// working.
+@class mozNotificationCenterDelegate;
+
+namespace mozilla {
+
+class OSXNotificationInfo;
+
+class OSXNotificationCenter : public nsIAlertsService,
+ public nsIAlertsIconData,
+ public nsIAlertsDoNotDisturb,
+ public nsIAlertNotificationImageListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_NSIALERTSICONDATA
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIALERTNOTIFICATIONIMAGELISTENER
+
+ OSXNotificationCenter();
+
+ nsresult Init();
+ void CloseAlertCocoaString(NSString* aAlertName);
+ void OnActivate(NSString* aAlertName, NSUserNotificationActivationType aActivationType,
+ unsigned long long aAdditionalActionIndex);
+ void ShowPendingNotification(OSXNotificationInfo* osxni);
+
+ protected:
+ virtual ~OSXNotificationCenter();
+
+ private:
+ mozNotificationCenterDelegate* mDelegate;
+ nsTArray<RefPtr<OSXNotificationInfo> > mActiveAlerts;
+ nsTArray<RefPtr<OSXNotificationInfo> > mPendingAlerts;
+ bool mSuppressForScreenSharing;
+};
+
+} // namespace mozilla
+
+#endif // OSXNotificationCenter_h
diff --git a/widget/cocoa/OSXNotificationCenter.mm b/widget/cocoa/OSXNotificationCenter.mm
new file mode 100644
index 0000000000..854bcadb13
--- /dev/null
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -0,0 +1,556 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "OSXNotificationCenter.h"
+#import <AppKit/AppKit.h>
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "nsICancelable.h"
+#include "nsIStringBundle.h"
+#include "nsNetUtil.h"
+#import "nsCocoaUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+
+using namespace mozilla;
+
+#define MAX_NOTIFICATION_NAME_LEN 5000
+
+@protocol FakeNSUserNotification <NSObject>
+@property(copy) NSString* title;
+@property(copy) NSString* subtitle;
+@property(copy) NSString* informativeText;
+@property(copy) NSString* actionButtonTitle;
+@property(copy) NSDictionary* userInfo;
+@property(copy) NSDate* deliveryDate;
+@property(copy) NSTimeZone* deliveryTimeZone;
+@property(copy) NSDateComponents* deliveryRepeatInterval;
+@property(readonly) NSDate* actualDeliveryDate;
+@property(readonly, getter=isPresented) BOOL presented;
+@property(readonly, getter=isRemote) BOOL remote;
+@property(copy) NSString* soundName;
+@property BOOL hasActionButton;
+@property(readonly) NSUserNotificationActivationType activationType;
+@property(copy) NSString* otherButtonTitle;
+@property(copy) NSImage* contentImage;
+@end
+
+@protocol FakeNSUserNotificationCenter <NSObject>
++ (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter;
+@property(assign) id<NSUserNotificationCenterDelegate> delegate;
+@property(copy) NSArray* scheduledNotifications;
+- (void)scheduleNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification;
+@property(readonly) NSArray* deliveredNotifications;
+- (void)deliverNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeAllDeliveredNotifications;
+- (void)_removeAllDisplayedNotifications;
+- (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification;
+@end
+
+@interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate> {
+ OSXNotificationCenter* mOSXNC;
+}
+- (id)initWithOSXNC:(OSXNotificationCenter*)osxnc;
+@end
+
+@implementation mozNotificationCenterDelegate
+
+- (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
+ [super init];
+ // We should *never* outlive this OSXNotificationCenter.
+ mOSXNC = osxnc;
+ return self;
+}
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didDeliverNotification:(id<FakeNSUserNotification>)notification {
+}
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didActivateNotification:(id<FakeNSUserNotification>)notification {
+ unsigned long long additionalActionIndex = ULLONG_MAX;
+ if ([notification respondsToSelector:@selector(_alternateActionIndex)]) {
+ NSNumber* alternateActionIndex = [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
+ additionalActionIndex = [alternateActionIndex unsignedLongLongValue];
+ }
+ mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"], notification.activationType,
+ additionalActionIndex);
+}
+
+- (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ shouldPresentNotification:(id<FakeNSUserNotification>)notification {
+ return YES;
+}
+
+// This is an undocumented method that we need for parity with Safari.
+// Apple bug #15440664.
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didRemoveDeliveredNotifications:(NSArray*)notifications {
+ for (id<FakeNSUserNotification> notification in notifications) {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ mOSXNC->CloseAlertCocoaString(name);
+ }
+}
+
+// This is an undocumented method that we need to be notified if a user clicks the close button.
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didDismissAlert:(id<FakeNSUserNotification>)notification {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ mOSXNC->CloseAlertCocoaString(name);
+}
+
+@end
+
+namespace mozilla {
+
+enum {
+ OSXNotificationActionDisable = 0,
+ OSXNotificationActionSettings = 1,
+};
+
+class OSXNotificationInfo final : public nsISupports {
+ private:
+ virtual ~OSXNotificationInfo();
+
+ public:
+ NS_DECL_ISUPPORTS
+ OSXNotificationInfo(NSString* name, nsIObserver* observer, const nsAString& alertCookie);
+
+ NSString* mName;
+ nsCOMPtr<nsIObserver> mObserver;
+ nsString mCookie;
+ RefPtr<nsICancelable> mIconRequest;
+ id<FakeNSUserNotification> mPendingNotification;
+};
+
+NS_IMPL_ISUPPORTS0(OSXNotificationInfo)
+
+OSXNotificationInfo::OSXNotificationInfo(NSString* name, nsIObserver* observer,
+ const nsAString& alertCookie) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!");
+ mName = [name retain];
+ mObserver = observer;
+ mCookie = alertCookie;
+ mPendingNotification = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+OSXNotificationInfo::~OSXNotificationInfo() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mName release];
+ [mPendingNotification release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static id<FakeNSUserNotificationCenter> GetNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Class c = NSClassFromString(@"NSUserNotificationCenter");
+ return [c performSelector:@selector(defaultUserNotificationCenter)];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+OSXNotificationCenter::OSXNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this];
+ GetNotificationCenter().delegate = mDelegate;
+ mSuppressForScreenSharing = false;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+OSXNotificationCenter::~OSXNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [GetNotificationCenter() removeAllDeliveredNotifications];
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData, nsIAlertsDoNotDisturb,
+ nsIAlertNotificationImageListener)
+
+nsresult OSXNotificationCenter::Init() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlertNotification(
+ const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText,
+ bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction) {
+ nsCOMPtr<nsIAlertNotification> alert = do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ // vibrate is unused for now
+ nsTArray<uint32_t> vibrate;
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable,
+ aAlertCookie, aBidi, aLang, aData, aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction, false, vibrate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowAlert(alert, aAlertListener);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowPersistentNotification(const nsAString& aPersistentData,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert, nsIObserver* aAlertListener) {
+ return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener, uint32_t aIconSize,
+ const uint8_t* aIconData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_ARG(aAlert);
+
+ if (mSuppressForScreenSharing) {
+ return NS_OK;
+ }
+
+ Class unClass = NSClassFromString(@"NSUserNotification");
+ id<FakeNSUserNotification> notification = [[unClass alloc] init];
+
+ nsAutoString title;
+ nsresult rv = aAlert->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notification.title = nsCocoaUtils::ToNSString(title);
+
+ nsAutoString hostPort;
+ rv = aAlert->GetSource(hostPort);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
+
+ if (!hostPort.IsEmpty() && bundle) {
+ AutoTArray<nsString, 1> formatStrings = {hostPort};
+ nsAutoString notificationSource;
+ bundle->FormatStringFromName("source.label", formatStrings, notificationSource);
+ notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
+ }
+
+ nsAutoString text;
+ rv = aAlert->GetText(text);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notification.informativeText = nsCocoaUtils::ToNSString(text);
+
+ bool isSilent;
+ aAlert->GetSilent(&isSilent);
+ notification.soundName = isSilent ? nil : NSUserNotificationDefaultSoundName;
+ notification.hasActionButton = NO;
+
+ // If this is not an application/extension alert, show additional actions dealing with
+ // permissions.
+ bool isActionable;
+ if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) && isActionable) {
+ nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
+ bundle->GetStringFromName("closeButton.title", closeButtonTitle);
+ bundle->GetStringFromName("actionButton.label", actionButtonTitle);
+ if (!hostPort.IsEmpty()) {
+ AutoTArray<nsString, 1> formatStrings = {hostPort};
+ bundle->FormatStringFromName("webActions.disableForOrigin.label", formatStrings,
+ disableButtonTitle);
+ }
+ bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
+
+ notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
+
+ // OS X 10.8 only shows action buttons if the "Alerts" style is set in
+ // Notification Center preferences, and doesn't support the alternate
+ // action menu.
+ if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
+ [notification respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
+ [notification respondsToSelector:@selector(set_alternateActionButtonTitles:)]) {
+ notification.hasActionButton = YES;
+ notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
+
+ [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
+ [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
+ [(NSObject*)notification setValue:@[
+ nsCocoaUtils::ToNSString(disableButtonTitle), nsCocoaUtils::ToNSString(settingsButtonTitle)
+ ]
+ forKey:@"_alternateActionButtonTitles"];
+ }
+ }
+ nsAutoString name;
+ rv = aAlert->GetName(name);
+ // Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
+ // More than that shouldn't be necessary and userInfo (assigned to below) has
+ // a length limit of 16k on OS X 10.11. Exception thrown if limit exceeded.
+ if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ NSString* alertName = nsCocoaUtils::ToNSString(name);
+ if (!alertName) {
+ return NS_ERROR_FAILURE;
+ }
+ notification.userInfo =
+ [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
+ forKeys:[NSArray arrayWithObjects:@"name", nil]];
+
+ nsAutoString cookie;
+ rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OSXNotificationInfo* osxni = new OSXNotificationInfo(alertName, aAlertListener, cookie);
+
+ // Show the favicon if supported on this version of OS X.
+ if (aIconSize > 0 && [notification respondsToSelector:@selector(set_identityImage:)] &&
+ [notification respondsToSelector:@selector(set_identityImageHasBorder:)]) {
+ NSData* iconData = [NSData dataWithBytes:aIconData length:aIconSize];
+ NSImage* icon = [[[NSImage alloc] initWithData:iconData] autorelease];
+
+ [(NSObject*)notification setValue:icon forKey:@"_identityImage"];
+ [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"];
+ }
+
+ bool inPrivateBrowsing;
+ rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Show the notification without waiting for an image if there is no icon URL or
+ // notification icons are not supported on this version of OS X.
+ if (![unClass instancesRespondToSelector:@selector(setContentImage:)]) {
+ CloseAlertCocoaString(alertName);
+ mActiveAlerts.AppendElement(osxni);
+ [GetNotificationCenter() deliverNotification:notification];
+ [notification release];
+ if (aAlertListener) {
+ aAlertListener->Observe(nullptr, "alertshow", cookie.get());
+ }
+ } else {
+ mPendingAlerts.AppendElement(osxni);
+ osxni->mPendingNotification = notification;
+ // Wait six seconds for the image to load.
+ rv = aAlert->LoadImage(6000, this, osxni, getter_AddRefs(osxni->mIconRequest));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ShowPendingNotification(osxni);
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, bool aContextClosed) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
+ CloseAlertCocoaString(alertName);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aAlertName) {
+ return; // Can't do anything without a name
+ }
+
+ NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
+ for (id<FakeNSUserNotification> notification in notifications) {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ if ([name isEqualToString:aAlertName]) {
+ [GetNotificationCenter() removeDeliveredNotification:notification];
+ [GetNotificationCenter() _removeDisplayedNotification:notification];
+ break;
+ }
+ }
+
+ for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
+ OSXNotificationInfo* osxni = mActiveAlerts[i];
+ if ([aAlertName isEqualToString:osxni->mName]) {
+ if (osxni->mObserver) {
+ osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get());
+ }
+ if (osxni->mIconRequest) {
+ osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
+ osxni->mIconRequest = nullptr;
+ }
+ mActiveAlerts.RemoveElementAt(i);
+ break;
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void OSXNotificationCenter::OnActivate(NSString* aAlertName,
+ NSUserNotificationActivationType aActivationType,
+ unsigned long long aAdditionalActionIndex) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aAlertName) {
+ return; // Can't do anything without a name
+ }
+
+ for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
+ OSXNotificationInfo* osxni = mActiveAlerts[i];
+ if ([aAlertName isEqualToString:osxni->mName]) {
+ if (osxni->mObserver) {
+ switch ((int)aActivationType) {
+ case NSUserNotificationActivationTypeAdditionalActionClicked:
+ case NSUserNotificationActivationTypeActionButtonClicked:
+ switch (aAdditionalActionIndex) {
+ case OSXNotificationActionDisable:
+ osxni->mObserver->Observe(nullptr, "alertdisablecallback", osxni->mCookie.get());
+ break;
+ case OSXNotificationActionSettings:
+ osxni->mObserver->Observe(nullptr, "alertsettingscallback", osxni->mCookie.get());
+ break;
+ default:
+ NS_WARNING("Unknown NSUserNotification additional action clicked");
+ break;
+ }
+ break;
+ default:
+ osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
+ break;
+ }
+ }
+ return;
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo* osxni) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (osxni->mIconRequest) {
+ osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
+ osxni->mIconRequest = nullptr;
+ }
+
+ CloseAlertCocoaString(osxni->mName);
+
+ for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) {
+ if (mPendingAlerts[i] == osxni) {
+ mActiveAlerts.AppendElement(osxni);
+ mPendingAlerts.RemoveElementAt(i);
+ break;
+ }
+ }
+
+ [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
+
+ if (osxni->mObserver) {
+ osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
+ }
+
+ [osxni->mPendingNotification release];
+ osxni->mPendingNotification = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
+ if (osxni->mPendingNotification) {
+ // If there was an error getting the image, or the request timed out, show
+ // the notification without a content image.
+ ShowPendingNotification(osxni);
+ }
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::OnImageReady(nsISupports* aUserData, imgIRequest* aRequest) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCOMPtr<imgIContainer> image;
+ nsresult rv = aRequest->GetImage(getter_AddRefs(image));
+ if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
+ return rv;
+ }
+
+ OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
+ if (!osxni->mPendingNotification) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSImage* cocoaImage = nil;
+ // TODO: Pass pres context / ComputedStyle here to support context paint properties
+ nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST,
+ nullptr, nullptr, &cocoaImage);
+ (osxni->mPendingNotification).contentImage = cocoaImage;
+ [cocoaImage release];
+ ShowPendingNotification(osxni);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// nsIAlertsDoNotDisturb
+NS_IMETHODIMP
+OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ NS_ENSURE_ARG(aRetVal);
+ *aRetVal = mSuppressForScreenSharing;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ mSuppressForScreenSharing = aSuppress;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+} // namespace mozilla
diff --git a/widget/cocoa/SDKDeclarations.h b/widget/cocoa/SDKDeclarations.h
new file mode 100644
index 0000000000..b064093582
--- /dev/null
+++ b/widget/cocoa/SDKDeclarations.h
@@ -0,0 +1,133 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef SDKDefines_h
+#define SDKDefines_h
+
+#import <Cocoa/Cocoa.h>
+
+/**
+ * This file contains header declarations from SDKs more recent than the minimum macOS SDK which we
+ * require for building Firefox, which is currently the macOS 10.12 SDK.
+ */
+
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+
+@interface NSView (NSView10_12_2)
+- (NSTouchBar*)makeTouchBar;
+@end
+
+#endif
+
+#if !defined(MAC_OS_X_VERSION_10_13) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13
+
+using NSAppearanceName = NSString*;
+
+@interface NSColor (NSColor10_13)
+// "Available in 10.10", but not present in any SDK less than 10.13
+@property(class, strong, readonly) NSColor* systemPurpleColor NS_AVAILABLE_MAC(10_10);
+@end
+
+@interface NSTask (NSTask10_13)
+@property(copy) NSURL* executableURL NS_AVAILABLE_MAC(10_13);
+@property(copy) NSArray<NSString*>* arguments;
+- (BOOL)launchAndReturnError:(NSError**)error NS_AVAILABLE_MAC(10_13);
+@end
+
+enum : OSType {
+ kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange = 'x420',
+ kCVPixelFormatType_420YpCbCr10BiPlanarFullRange = 'xf20',
+};
+
+#endif
+
+#if !defined(MAC_OS_X_VERSION_10_14) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_14
+
+const NSAppearanceName NSAppearanceNameDarkAqua = @"NSAppearanceNameDarkAqua";
+
+@interface NSWindow (NSWindow10_14)
+@property(weak) NSObject<NSAppearanceCustomization>* appearanceSource NS_AVAILABLE_MAC(10_14);
+@end
+
+@interface NSApplication (NSApplication10_14)
+@property(strong) NSAppearance* appearance NS_AVAILABLE_MAC(10_14);
+@property(readonly, strong) NSAppearance* effectiveAppearance NS_AVAILABLE_MAC(10_14);
+@end
+
+@interface NSAppearance (NSAppearance10_14)
+- (NSAppearanceName)bestMatchFromAppearancesWithNames:(NSArray<NSAppearanceName>*)appearances
+ NS_AVAILABLE_MAC(10_14);
+@end
+
+@interface NSColor (NSColor10_14)
+// Available in 10.10, but retroactively made public in 10.14.
+@property(class, strong, readonly) NSColor* linkColor NS_AVAILABLE_MAC(10_10);
+@end
+
+enum {
+ NSVisualEffectMaterialToolTip NS_ENUM_AVAILABLE_MAC(10_14) = 17,
+};
+
+#endif
+
+#if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+// The declarations below do not have NS_AVAILABLE_MAC(11_0) on them because we're building with a
+// pre-macOS 11 SDK, so macOS 11 identifies itself as 10.16, and @available(macOS 11.0, *) checks
+// won't work. You'll need to use an annoying double-whammy check for these:
+//
+// #if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+// if (nsCocoaFeatures::OnBigSurOrLater()) {
+// #else
+// if (@available(macOS 11.0, *)) {
+// #endif
+// ...
+// }
+//
+
+typedef NS_ENUM(NSInteger, NSTitlebarSeparatorStyle) {
+ NSTitlebarSeparatorStyleAutomatic,
+ NSTitlebarSeparatorStyleNone,
+ NSTitlebarSeparatorStyleLine,
+ NSTitlebarSeparatorStyleShadow
+};
+
+@interface NSWindow (NSWindow11_0)
+@property NSTitlebarSeparatorStyle titlebarSeparatorStyle;
+@end
+
+@interface NSMenu (NSMenu11_0)
+// In reality, NSMenu implements the NSAppearanceCustomization protocol, and picks up the appearance
+// property from that protocol. But we can't tack on protocol implementations, so we just declare
+// the property setter here.
+- (void)setAppearance:(NSAppearance*)appearance;
+@end
+
+#endif
+
+#if !defined(MAC_OS_VERSION_12_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_12_0
+
+typedef CFTypeRef AXTextMarkerRef;
+typedef CFTypeRef AXTextMarkerRangeRef;
+
+extern "C" {
+CFTypeID AXTextMarkerGetTypeID();
+AXTextMarkerRef AXTextMarkerCreate(CFAllocatorRef allocator, const UInt8* bytes, CFIndex length);
+const UInt8* AXTextMarkerGetBytePtr(AXTextMarkerRef text_marker);
+CFIndex AXTextMarkerGetLength(AXTextMarkerRef text_marker);
+CFTypeID AXTextMarkerRangeGetTypeID();
+AXTextMarkerRangeRef AXTextMarkerRangeCreate(CFAllocatorRef allocator, AXTextMarkerRef start_marker,
+ AXTextMarkerRef end_marker);
+AXTextMarkerRef AXTextMarkerRangeCopyStartMarker(AXTextMarkerRangeRef text_marker_range);
+AXTextMarkerRef AXTextMarkerRangeCopyEndMarker(AXTextMarkerRangeRef text_marker_range);
+}
+
+@interface NSScreen (NSScreen12_0)
+// https://developer.apple.com/documentation/appkit/nsscreen/3882821-safeareainsets?language=objc&changes=latest_major
+@property(readonly) NSEdgeInsets safeAreaInsets;
+@end
+
+#endif
+
+#endif // SDKDefines_h
diff --git a/widget/cocoa/ScreenHelperCocoa.h b/widget/cocoa/ScreenHelperCocoa.h
new file mode 100644
index 0000000000..91f4a19677
--- /dev/null
+++ b/widget/cocoa/ScreenHelperCocoa.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_cocoa_ScreenHelperCocoa_h
+#define mozilla_widget_cocoa_ScreenHelperCocoa_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+@class ScreenHelperDelegate;
+@class NSScreen;
+
+namespace mozilla {
+namespace widget {
+
+class ScreenHelperCocoa final : public ScreenManager::Helper {
+ public:
+ ScreenHelperCocoa();
+ ~ScreenHelperCocoa() override;
+
+ void RefreshScreens();
+
+ static NSScreen* CocoaScreenForScreen(nsIScreen* aScreen);
+
+ private:
+ ScreenHelperDelegate* mDelegate;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_gtk_ScreenHelperGtk_h
diff --git a/widget/cocoa/ScreenHelperCocoa.mm b/widget/cocoa/ScreenHelperCocoa.mm
new file mode 100644
index 0000000000..27c18bbfdf
--- /dev/null
+++ b/widget/cocoa/ScreenHelperCocoa.mm
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ScreenHelperCocoa.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/Logging.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+static LazyLogModule sScreenLog("WidgetScreen");
+
+@interface ScreenHelperDelegate : NSObject {
+ @private
+ mozilla::widget::ScreenHelperCocoa* mHelper;
+}
+
+- (id)initWithScreenHelper:(mozilla::widget::ScreenHelperCocoa*)aScreenHelper;
+- (void)didChangeScreenParameters:(NSNotification*)aNotification;
+@end
+
+@implementation ScreenHelperDelegate
+- (id)initWithScreenHelper:(mozilla::widget::ScreenHelperCocoa*)aScreenHelper {
+ if ((self = [self init])) {
+ mHelper = aScreenHelper;
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(didChangeScreenParameters:)
+ name:NSApplicationDidChangeScreenParametersNotification
+ object:nil];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)didChangeScreenParameters:(NSNotification*)aNotification {
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("Received NSApplicationDidChangeScreenParametersNotification"));
+
+ mHelper->RefreshScreens();
+}
+@end
+
+namespace mozilla {
+namespace widget {
+
+ScreenHelperCocoa::ScreenHelperCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("ScreenHelperCocoa created"));
+
+ mDelegate = [[ScreenHelperDelegate alloc] initWithScreenHelper:this];
+
+ RefreshScreens();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+ScreenHelperCocoa::~ScreenHelperCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static already_AddRefed<Screen> MakeScreen(NSScreen* aScreen) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ DesktopToLayoutDeviceScale contentsScaleFactor(nsCocoaUtils::GetBackingScaleFactor(aScreen));
+ CSSToLayoutDeviceScale defaultCssScaleFactor(contentsScaleFactor.scale);
+ NSRect frame = [aScreen frame];
+ LayoutDeviceIntRect rect =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, contentsScaleFactor.scale);
+ frame = [aScreen visibleFrame];
+ LayoutDeviceIntRect availRect =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, contentsScaleFactor.scale);
+
+ // aScreen may be capable of displaying multiple pixel depths, for example by
+ // transitioning to an HDR-capable depth when required by a window displayed on
+ // the screen. We want to note the maximum capabilities of the screen, so we use
+ // the largest depth it offers.
+ uint32_t pixelDepth = 0;
+ const NSWindowDepth* depths = [aScreen supportedWindowDepths];
+ for (size_t d = 0; NSWindowDepth depth = depths[d]; d++) {
+ uint32_t bpp = NSBitsPerPixelFromDepth(depth);
+ if (bpp > pixelDepth) {
+ pixelDepth = bpp;
+ }
+ }
+
+ // But it confuses content if we return too-high a value here. Cap depth with
+ // a value that matches what Chrome returns for high bpp screens.
+ static const uint32_t MAX_REPORTED_PIXEL_DEPTH = 30;
+ if (pixelDepth > MAX_REPORTED_PIXEL_DEPTH) {
+ pixelDepth = MAX_REPORTED_PIXEL_DEPTH;
+ }
+
+ float dpi = 96.0f;
+ CGDirectDisplayID displayID =
+ [[[aScreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue];
+ CGFloat heightMM = ::CGDisplayScreenSize(displayID).height;
+ if (heightMM > 0) {
+ dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT);
+ }
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("New screen [%d %d %d %d (%d %d %d %d) %d %f %f %f]", rect.x, rect.y, rect.width,
+ rect.height, availRect.x, availRect.y, availRect.width, availRect.height, pixelDepth,
+ contentsScaleFactor.scale, defaultCssScaleFactor.scale, dpi));
+
+ // Getting the refresh rate is a little hard on OS X. We could use
+ // CVDisplayLinkGetNominalOutputVideoRefreshPeriod, but that's a little
+ // involved. Ideally we could query it from vsync. For now, we leave it out.
+ RefPtr<Screen> screen =
+ new Screen(rect, availRect, pixelDepth, pixelDepth, 0, contentsScaleFactor,
+ defaultCssScaleFactor, dpi, Screen::IsPseudoDisplay::No);
+ return screen.forget();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+void ScreenHelperCocoa::RefreshScreens() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens"));
+
+ AutoTArray<RefPtr<Screen>, 4> screens;
+
+ for (NSScreen* screen in [NSScreen screens]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ screens.AppendElement(MakeScreen(screen));
+ }
+
+ ScreenManager::Refresh(std::move(screens));
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NSScreen* ScreenHelperCocoa::CocoaScreenForScreen(nsIScreen* aScreen) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ for (NSScreen* screen in [NSScreen screens]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ LayoutDeviceIntRect rect;
+ double scale;
+ aScreen->GetRect(&rect.x, &rect.y, &rect.width, &rect.height);
+ aScreen->GetContentsScaleFactor(&scale);
+ NSRect frame = [screen frame];
+ LayoutDeviceIntRect frameRect = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, scale);
+ if (rect == frameRect) {
+ return screen;
+ }
+ }
+ return [NSScreen mainScreen];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/TextInputHandler.h b/widget/cocoa/TextInputHandler.h
new file mode 100644
index 0000000000..91ca29a901
--- /dev/null
+++ b/widget/cocoa/TextInputHandler.h
@@ -0,0 +1,1283 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* 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/. */
+
+#ifndef TextInputHandler_h_
+#define TextInputHandler_h_
+
+#include "nsCocoaUtils.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#include "mozView.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "WritingModes.h"
+
+class nsChildView;
+
+namespace mozilla {
+namespace widget {
+
+// Key code constants
+enum {
+ kVK_PC_PrintScreen = kVK_F13,
+ kVK_PC_ScrollLock = kVK_F14,
+ kVK_PC_Pause = kVK_F15,
+
+ kVK_PC_Insert = kVK_Help,
+ kVK_PC_Backspace = kVK_Delete,
+ kVK_PC_Delete = kVK_ForwardDelete,
+
+ kVK_PC_ContextMenu = 0x6E,
+
+ kVK_Powerbook_KeypadEnter = 0x34 // Enter on Powerbook's keyboard is different
+};
+
+/**
+ * TISInputSourceWrapper is a wrapper for the TISInputSourceRef. If we get the
+ * TISInputSourceRef from InputSourceID, we need to release the CFArray instance
+ * which is returned by TISCreateInputSourceList. However, when we release the
+ * list, we cannot access the TISInputSourceRef. So, it's not usable, and it
+ * may cause the memory leak bugs. nsTISInputSource automatically releases the
+ * list when the instance is destroyed.
+ */
+class TISInputSourceWrapper {
+ public:
+ static TISInputSourceWrapper& CurrentInputSource();
+ /**
+ * Shutdown() should be called when nobody doesn't need to use this class.
+ */
+ static void Shutdown();
+
+ TISInputSourceWrapper()
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ Clear();
+ }
+
+ explicit TISInputSourceWrapper(const char* aID)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByInputSourceID(aID);
+ }
+
+ explicit TISInputSourceWrapper(SInt32 aLayoutID)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByLayoutID(aLayoutID);
+ }
+
+ explicit TISInputSourceWrapper(TISInputSourceRef aInputSource)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByTISInputSourceRef(aInputSource);
+ }
+
+ ~TISInputSourceWrapper() { Clear(); }
+
+ void InitByInputSourceID(const char* aID);
+ void InitByInputSourceID(const nsString& aID);
+ void InitByInputSourceID(const CFStringRef aID);
+ /**
+ * InitByLayoutID() initializes the keyboard layout by the layout ID.
+ *
+ * @param aLayoutID An ID of keyboard layout.
+ * 0: US
+ * 1: Greek
+ * 2: German
+ * 3: Swedish-Pro
+ * 4: Dvorak-Qwerty Cmd
+ * 5: Thai
+ * 6: Arabic
+ * 7: French
+ * 8: Hebrew
+ * 9: Lithuanian
+ * 10: Norwegian
+ * 11: Spanish
+ * @param aOverrideKeyboard When testing set to TRUE, otherwise, set to
+ * FALSE. When TRUE, we use an ANSI keyboard
+ * instead of the actual keyboard.
+ */
+ void InitByLayoutID(SInt32 aLayoutID, bool aOverrideKeyboard = false);
+ void InitByCurrentInputSource();
+ void InitByCurrentKeyboardLayout();
+ void InitByCurrentASCIICapableInputSource();
+ void InitByCurrentASCIICapableKeyboardLayout();
+ void InitByCurrentInputMethodKeyboardLayoutOverride();
+ void InitByTISInputSourceRef(TISInputSourceRef aInputSource);
+ void InitByLanguage(CFStringRef aLanguage);
+
+ /**
+ * If the instance is initialized with a keyboard layout input source,
+ * returns it.
+ * If the instance is initialized with an IME mode input source, the result
+ * references the keyboard layout for the IME mode. However, this can be
+ * initialized only when the IME mode is actually selected. I.e, if IME mode
+ * input source is initialized with LayoutID or SourceID, this returns null.
+ */
+ TISInputSourceRef GetKeyboardLayoutInputSource() const { return mKeyboardLayout; }
+ const UCKeyboardLayout* GetUCKeyboardLayout();
+
+ bool IsOpenedIMEMode();
+ bool IsIMEMode();
+ bool IsKeyboardLayout();
+
+ bool IsASCIICapable() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetBoolProperty(kTISPropertyInputSourceIsASCIICapable);
+ }
+
+ bool IsEnabled() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetBoolProperty(kTISPropertyInputSourceIsEnabled);
+ }
+
+ bool GetLanguageList(CFArrayRef& aLanguageList);
+ bool GetPrimaryLanguage(CFStringRef& aPrimaryLanguage);
+ bool GetPrimaryLanguage(nsAString& aPrimaryLanguage);
+
+ bool GetLocalizedName(CFStringRef& aName) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyLocalizedName, aName);
+ }
+
+ bool GetLocalizedName(nsAString& aName) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyLocalizedName, aName);
+ }
+
+ bool GetInputSourceID(CFStringRef& aID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceID, aID);
+ }
+
+ bool GetInputSourceID(nsAString& aID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceID, aID);
+ }
+
+ bool GetBundleID(CFStringRef& aBundleID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyBundleID, aBundleID);
+ }
+
+ bool GetBundleID(nsAString& aBundleID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyBundleID, aBundleID);
+ }
+
+ bool GetInputSourceType(CFStringRef& aType) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceType, aType);
+ }
+
+ bool GetInputSourceType(nsAString& aType) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceType, aType);
+ }
+
+ bool IsForRTLLanguage();
+ bool IsForJapaneseLanguage();
+ bool IsInitializedByCurrentInputSource();
+
+ enum {
+ // 40 is an actual result of the ::LMGetKbdType() when we connect an
+ // unknown keyboard and set the keyboard type to ANSI manually on the
+ // set up dialog.
+ eKbdType_ANSI = 40
+ };
+
+ void Select();
+ void Clear();
+
+ /**
+ * InitKeyEvent() initializes aKeyEvent for aNativeKeyEvent.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event.
+ * @param aIsProcessedByIME true if aNativeKeyEvent has been handled
+ * by IME (but except if the composition was
+ * started with dead key).
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ */
+ void InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString = nullptr);
+
+ /**
+ * WillDispatchKeyboardEvent() computes aKeyEvent.mAlternativeCharCodes and
+ * recompute aKeyEvent.mCharCode if it's necessary.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ * @param aIndexOfKeypress Index of the eKeyPress event. If a key
+ * inputs 2 or more characters, eKeyPress events
+ * are dispatched for each character. This is
+ * 0 for the first eKeyPress event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event. This must be
+ * eKeyPress event.
+ */
+ void WillDispatchKeyboardEvent(NSEvent* aNativeKeyEvent, const nsAString* aInsertString,
+ uint32_t aIndexOfKeypress, WidgetKeyboardEvent& aKeyEvent);
+
+ /**
+ * ComputeGeckoKeyCode() returns Gecko keycode for aNativeKeyCode on current
+ * keyboard layout.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @param aCmdIsPressed TRUE if Cmd key is pressed. Otherwise, FALSE.
+ * @return The computed Gecko keycode.
+ */
+ uint32_t ComputeGeckoKeyCode(UInt32 aNativeKeyCode, UInt32 aKbType, bool aCmdIsPressed);
+
+ /**
+ * ComputeGeckoKeyNameIndex() returns Gecko key name index for the key.
+ *
+ * @param aNativeKeyCode A native keycode.
+ */
+ static KeyNameIndex ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode);
+
+ /**
+ * ComputeGeckoCodeNameIndex() returns Gecko code name index for the key.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ */
+ static CodeNameIndex ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode, UInt32 aKbType);
+
+ /**
+ * TranslateToChar() checks if aNativeKeyEvent is a dead key.
+ *
+ * @param aNativeKeyEvent A native key event.
+ * @return Returns true if the key event is a dead key
+ * event. Otherwise, false.
+ */
+ bool IsDeadKey(NSEvent* aNativeKeyEvent);
+
+ protected:
+ /**
+ * TranslateToString() computes the inputted text from the native keyCode,
+ * modifier flags and keyboard type.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @param aStr Result, i.e., inputted text.
+ * The result can be two or more characters.
+ * @return If succeeded, TRUE. Otherwise, FALSE.
+ * Even if TRUE, aStr can be empty string.
+ */
+ bool TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType, nsAString& aStr);
+
+ /**
+ * TranslateToChar() computes the inputted character from the native keyCode,
+ * modifier flags and keyboard type. If two or more characters would be
+ * input, this returns 0.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @return If succeeded and the result is one character,
+ * returns the charCode of it. Otherwise,
+ * returns 0.
+ */
+ uint32_t TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType);
+
+ /**
+ * TranslateToChar() checks if aKeyCode with aModifiers is a dead key.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @return Returns true if the key with specified
+ * modifier state is a dead key. Otherwise,
+ * false.
+ */
+ bool IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType);
+
+ /**
+ * ComputeInsertString() computes string to be inserted with the key event.
+ *
+ * @param aNativeKeyEvent The native key event which causes our keyboard
+ * event(s).
+ * @param aKeyEvent A Gecko key event which was partially
+ * initialized with aNativeKeyEvent.
+ * @param aInsertString The string to be inputting by aNativeKeyEvent.
+ * This should be specified by InsertText().
+ * In other words, if the key event doesn't cause
+ * a call of InsertText(), this can be nullptr.
+ * @param aResult The string which should be set to charCode of
+ * keypress event(s).
+ */
+ void ComputeInsertStringForCharCode(NSEvent* aNativeKeyEvent,
+ const WidgetKeyboardEvent& aKeyEvent,
+ const nsAString* aInsertString, nsAString& aResult);
+
+ /**
+ * IsPrintableKeyEvent() returns true if aNativeKeyEvent is caused by
+ * a printable key. Otherwise, returns false.
+ */
+ bool IsPrintableKeyEvent(NSEvent* aNativeKeyEvent) const;
+
+ /**
+ * GetKbdType() returns physical keyboard type.
+ */
+ UInt32 GetKbdType() const;
+
+ bool GetBoolProperty(const CFStringRef aKey);
+ bool GetStringProperty(const CFStringRef aKey, CFStringRef& aStr);
+ bool GetStringProperty(const CFStringRef aKey, nsAString& aStr);
+
+ TISInputSourceRef mInputSource;
+ TISInputSourceRef mKeyboardLayout;
+ CFArrayRef mInputSourceList;
+ const UCKeyboardLayout* mUCKeyboardLayout;
+ int8_t mIsRTL;
+
+ bool mOverrideKeyboard;
+
+ static TISInputSourceWrapper* sCurrentInputSource;
+};
+
+/**
+ * TextInputHandlerBase is a base class of IMEInputHandler and TextInputHandler.
+ * Utility methods should be implemented this level.
+ */
+
+class TextInputHandlerBase : public TextEventDispatcherListener {
+ public:
+ /**
+ * Other TextEventDispatcherListener methods should be implemented in
+ * IMEInputHandler.
+ */
+ NS_DECL_ISUPPORTS
+
+ /**
+ * DispatchEvent() dispatches aEvent on mWidget.
+ *
+ * @param aEvent An event which you want to dispatch.
+ * @return TRUE if the event is consumed by web contents
+ * or chrome contents. Otherwise, FALSE.
+ */
+ bool DispatchEvent(WidgetGUIEvent& aEvent);
+
+ /**
+ * SetSelection() dispatches eSetSelection event for the aRange.
+ *
+ * @param aRange The range which will be selected.
+ * @return TRUE if setting selection is succeeded and
+ * the widget hasn't been destroyed.
+ * Otherwise, FALSE.
+ */
+ bool SetSelection(NSRange& aRange);
+
+ /**
+ * InitKeyEvent() initializes aKeyEvent for aNativeKeyEvent.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event.
+ * @param aIsProcessedByIME true if aNativeKeyEvent has been handled
+ * by IME (but except if the composition was
+ * started with dead key).
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ */
+ void InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString = nullptr);
+
+ /**
+ * SynthesizeNativeKeyEvent() is an implementation of
+ * nsIWidget::SynthesizeNativeKeyEvent(). See the document in nsIWidget.h
+ * for the detail.
+ */
+ nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters);
+
+ /**
+ * Utility method intended for testing. Attempts to construct a native key
+ * event that would have been generated during an actual key press. This
+ * *does not dispatch* the native event. Instead, it is attached to the
+ * |mNativeKeyEvent| field of the Gecko event that is passed in.
+ * @param aKeyEvent Gecko key event to attach the native event to
+ */
+ NS_IMETHOD AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent);
+
+ /**
+ * GetWindowLevel() returns the window level of current focused (in Gecko)
+ * window. E.g., if an <input> element in XUL panel has focus, this returns
+ * the XUL panel's window level.
+ */
+ NSInteger GetWindowLevel();
+
+ /**
+ * IsSpecialGeckoKey() checks whether aNativeKeyCode is mapped to a special
+ * Gecko keyCode. A key is "special" if it isn't used for text input.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @return If the keycode is mapped to a special key,
+ * TRUE. Otherwise, FALSE.
+ */
+ static bool IsSpecialGeckoKey(UInt32 aNativeKeyCode);
+
+ /**
+ * EnableSecureEventInput() and DisableSecureEventInput() wrap the Carbon
+ * Event Manager APIs with the same names. In addition they keep track of
+ * how many times we've called them (in the same process) -- unlike the
+ * Carbon Event Manager APIs, which only keep track of how many times they've
+ * been called from any and all processes.
+ *
+ * The Carbon Event Manager's IsSecureEventInputEnabled() returns whether
+ * secure event input mode is enabled (in any process). This class's
+ * IsSecureEventInputEnabled() returns whether we've made any calls to
+ * EnableSecureEventInput() that are not (yet) offset by the calls we've
+ * made to DisableSecureEventInput().
+ */
+ static void EnableSecureEventInput();
+ static void DisableSecureEventInput();
+ static bool IsSecureEventInputEnabled();
+
+ /**
+ * EnsureSecureEventInputDisabled() calls DisableSecureEventInput() until
+ * our call count becomes 0.
+ */
+ static void EnsureSecureEventInputDisabled();
+
+ public:
+ /**
+ * mWidget must not be destroyed without OnDestroyWidget being called.
+ *
+ * @param aDestroyingWidget Destroying widget. This might not be mWidget.
+ * @return This result doesn't have any meaning for
+ * callers. When aDstroyingWidget isn't the same
+ * as mWidget, FALSE. Then, inherited methods in
+ * sub classes should return from this method
+ * without cleaning up.
+ */
+ virtual bool OnDestroyWidget(nsChildView* aDestroyingWidget);
+
+ protected:
+ // The creator of this instance, client and its text event dispatcher.
+ // These members must not be nullptr after initialized until
+ // OnDestroyWidget() is called.
+ nsChildView* mWidget; // [WEAK]
+ RefPtr<TextEventDispatcher> mDispatcher;
+
+ // The native view for mWidget.
+ // This view handles the actual text inputting.
+ NSView<mozView>* mView; // [STRONG]
+
+ TextInputHandlerBase(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~TextInputHandlerBase();
+
+ bool Destroyed() { return !mWidget; }
+
+ /**
+ * mCurrentKeyEvent indicates what key event we are handling. While
+ * handling a native keydown event, we need to store the event for insertText,
+ * doCommandBySelector and various action message handlers of NSResponder
+ * such as [NSResponder insertNewline:sender].
+ */
+ struct KeyEventState {
+ // Handling native key event
+ NSEvent* mKeyEvent;
+ // String specified by InsertText(). This is not null only during a
+ // call of InsertText().
+ nsAString* mInsertString;
+ // String which are included in [mKeyEvent characters] and already handled
+ // by InsertText() call(s).
+ nsString mInsertedString;
+ // Unique id associated with a keydown / keypress event. It's ok if this
+ // wraps over long periods.
+ uint32_t mUniqueId;
+ // Whether keydown event was dispatched for mKeyEvent.
+ bool mKeyDownDispatched;
+ // Whether keydown event was consumed by web contents or chrome contents.
+ bool mKeyDownHandled;
+ // Whether keypress event was dispatched for mKeyEvent.
+ bool mKeyPressDispatched;
+ // Whether keypress event was consumed by web contents or chrome contents.
+ bool mKeyPressHandled;
+ // Whether the key event causes other key events via IME or something.
+ bool mCausedOtherKeyEvents;
+ // Whether the key event causes composition change or committing
+ // composition. So, even if InsertText() is called, this may be false
+ // if it dispatches keypress event.
+ bool mCompositionDispatched;
+
+ KeyEventState() : mKeyEvent(nullptr), mUniqueId(0) { Clear(); }
+
+ explicit KeyEventState(NSEvent* aNativeKeyEvent, uint32_t aUniqueId = 0)
+ : mKeyEvent(nullptr), mUniqueId(0) {
+ Clear();
+ Set(aNativeKeyEvent, aUniqueId);
+ }
+
+ KeyEventState(const KeyEventState& aOther) = delete;
+
+ ~KeyEventState() { Clear(); }
+
+ void Set(NSEvent* aNativeKeyEvent, uint32_t aUniqueId = 0) {
+ MOZ_ASSERT(aNativeKeyEvent, "aNativeKeyEvent must not be NULL");
+ Clear();
+ mKeyEvent = [aNativeKeyEvent retain];
+ mUniqueId = aUniqueId;
+ }
+
+ void Clear() {
+ if (mKeyEvent) {
+ [mKeyEvent release];
+ mKeyEvent = nullptr;
+ mUniqueId = 0;
+ }
+ mInsertString = nullptr;
+ mInsertedString.Truncate();
+ mKeyDownDispatched = false;
+ mKeyDownHandled = false;
+ mKeyPressDispatched = false;
+ mKeyPressHandled = false;
+ mCausedOtherKeyEvents = false;
+ mCompositionDispatched = false;
+ }
+
+ bool IsDefaultPrevented() const {
+ return mKeyDownHandled || mKeyPressHandled || mCausedOtherKeyEvents || mCompositionDispatched;
+ }
+
+ bool CanDispatchKeyDownEvent() const { return !mKeyDownDispatched; }
+
+ bool CanDispatchKeyPressEvent() const { return !mKeyPressDispatched && !IsDefaultPrevented(); }
+
+ bool CanHandleCommand() const { return !mKeyDownHandled && !mKeyPressHandled; }
+
+ bool IsProperKeyEvent(Command aCommand) const {
+ if (NS_WARN_IF(!mKeyEvent)) {
+ return false;
+ }
+ KeyNameIndex keyNameIndex =
+ TISInputSourceWrapper::ComputeGeckoKeyNameIndex([mKeyEvent keyCode]);
+ Modifiers modifiers = nsCocoaUtils::ModifiersForEvent(mKeyEvent) &
+ (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ return keyNameIndex == KEY_NAME_INDEX_Enter && modifiers == MODIFIER_CONTROL;
+ case Command::InsertParagraph:
+ return keyNameIndex == KEY_NAME_INDEX_Enter && modifiers == MODIFIER_NONE;
+ case Command::DeleteCharBackward:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace && modifiers == MODIFIER_NONE;
+ case Command::DeleteToBeginningOfLine:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace && modifiers == MODIFIER_META;
+ case Command::DeleteWordBackward:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace && modifiers == MODIFIER_ALT;
+ case Command::DeleteCharForward:
+ return keyNameIndex == KEY_NAME_INDEX_Delete && modifiers == MODIFIER_NONE;
+ case Command::DeleteWordForward:
+ return keyNameIndex == KEY_NAME_INDEX_Delete && modifiers == MODIFIER_ALT;
+ case Command::InsertTab:
+ return keyNameIndex == KEY_NAME_INDEX_Tab && modifiers == MODIFIER_NONE;
+ case Command::InsertBacktab:
+ return keyNameIndex == KEY_NAME_INDEX_Tab && modifiers == MODIFIER_SHIFT;
+ case Command::CharNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight && modifiers == MODIFIER_NONE;
+ case Command::SelectCharNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight && modifiers == MODIFIER_SHIFT;
+ case Command::WordNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight && modifiers == MODIFIER_ALT;
+ case Command::SelectWordNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == (MODIFIER_ALT | MODIFIER_SHIFT);
+ case Command::EndLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight && modifiers == MODIFIER_META;
+ case Command::SelectEndLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT);
+ case Command::CharPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft && modifiers == MODIFIER_NONE;
+ case Command::SelectCharPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft && modifiers == MODIFIER_SHIFT;
+ case Command::WordPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft && modifiers == MODIFIER_ALT;
+ case Command::SelectWordPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == (MODIFIER_ALT | MODIFIER_SHIFT);
+ case Command::BeginLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft && modifiers == MODIFIER_META;
+ case Command::SelectBeginLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT);
+ case Command::LinePrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp && modifiers == MODIFIER_NONE;
+ case Command::SelectLinePrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp && modifiers == MODIFIER_SHIFT;
+ case Command::MoveTop:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp && modifiers == MODIFIER_META;
+ case Command::SelectTop:
+ return (keyNameIndex == KEY_NAME_INDEX_ArrowUp &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT)) ||
+ (keyNameIndex == KEY_NAME_INDEX_Home && modifiers == MODIFIER_SHIFT);
+ case Command::LineNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown && modifiers == MODIFIER_NONE;
+ case Command::SelectLineNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown && modifiers == MODIFIER_SHIFT;
+ case Command::MoveBottom:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown && modifiers == MODIFIER_META;
+ case Command::SelectBottom:
+ return (keyNameIndex == KEY_NAME_INDEX_ArrowDown &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT)) ||
+ (keyNameIndex == KEY_NAME_INDEX_End && modifiers == MODIFIER_SHIFT);
+ case Command::ScrollPageUp:
+ return keyNameIndex == KEY_NAME_INDEX_PageUp && modifiers == MODIFIER_NONE;
+ case Command::SelectPageUp:
+ return keyNameIndex == KEY_NAME_INDEX_PageUp && modifiers == MODIFIER_SHIFT;
+ case Command::ScrollPageDown:
+ return keyNameIndex == KEY_NAME_INDEX_PageDown && modifiers == MODIFIER_NONE;
+ case Command::SelectPageDown:
+ return keyNameIndex == KEY_NAME_INDEX_PageDown && modifiers == MODIFIER_SHIFT;
+ case Command::ScrollBottom:
+ return keyNameIndex == KEY_NAME_INDEX_End && modifiers == MODIFIER_NONE;
+ case Command::ScrollTop:
+ return keyNameIndex == KEY_NAME_INDEX_Home && modifiers == MODIFIER_NONE;
+ case Command::CancelOperation:
+ return (keyNameIndex == KEY_NAME_INDEX_Escape &&
+ (modifiers == MODIFIER_NONE || modifiers == MODIFIER_SHIFT)) ||
+ ([mKeyEvent keyCode] == kVK_ANSI_Period && modifiers == MODIFIER_META);
+ case Command::Complete:
+ return keyNameIndex == KEY_NAME_INDEX_Escape &&
+ (modifiers == MODIFIER_ALT || modifiers == (MODIFIER_ALT | MODIFIER_SHIFT));
+ default:
+ return false;
+ }
+ }
+
+ void InitKeyEvent(TextInputHandlerBase* aHandler, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME);
+
+ /**
+ * GetUnhandledString() returns characters of the event which have not been
+ * handled with InsertText() yet. For example, if there is a composition
+ * caused by a dead key press like '`' and it's committed by some key
+ * combinations like |Cmd+v|, then, the |v|'s KeyDown event's |characters|
+ * is |`v|. Then, after |`| is committed with a call of InsertString(),
+ * this returns only 'v'.
+ */
+ void GetUnhandledString(nsAString& aUnhandledString) const;
+ };
+
+ /**
+ * Helper classes for guaranteeing cleaning mCurrentKeyEvent
+ */
+ class AutoKeyEventStateCleaner {
+ public:
+ explicit AutoKeyEventStateCleaner(TextInputHandlerBase* aHandler) : mHandler(aHandler) {}
+
+ ~AutoKeyEventStateCleaner() { mHandler->RemoveCurrentKeyEvent(); }
+
+ private:
+ RefPtr<TextInputHandlerBase> mHandler;
+ };
+
+ class MOZ_STACK_CLASS AutoInsertStringClearer {
+ public:
+ explicit AutoInsertStringClearer(KeyEventState* aState) : mState(aState) {}
+ ~AutoInsertStringClearer();
+
+ private:
+ KeyEventState* mState;
+ };
+
+ /**
+ * mCurrentKeyEvents stores all key events which are being processed.
+ * When we call interpretKeyEvents, IME may generate other key events.
+ * mCurrentKeyEvents[0] is the latest key event.
+ */
+ nsTArray<KeyEventState*> mCurrentKeyEvents;
+
+ /**
+ * mFirstKeyEvent must be used for first key event. This member prevents
+ * memory fragmentation for most key events.
+ */
+ KeyEventState mFirstKeyEvent;
+
+ /**
+ * PushKeyEvent() adds the current key event to mCurrentKeyEvents.
+ */
+ KeyEventState* PushKeyEvent(NSEvent* aNativeKeyEvent, uint32_t aUniqueId = 0) {
+ uint32_t nestCount = mCurrentKeyEvents.Length();
+ for (uint32_t i = 0; i < nestCount; i++) {
+ // When the key event is caused by another key event, all key events
+ // which are being handled should be marked as "consumed".
+ mCurrentKeyEvents[i]->mCausedOtherKeyEvents = true;
+ }
+
+ KeyEventState* keyEvent = nullptr;
+ if (nestCount == 0) {
+ mFirstKeyEvent.Set(aNativeKeyEvent, aUniqueId);
+ keyEvent = &mFirstKeyEvent;
+ } else {
+ keyEvent = new KeyEventState(aNativeKeyEvent, aUniqueId);
+ }
+ return *mCurrentKeyEvents.AppendElement(keyEvent);
+ }
+
+ /**
+ * RemoveCurrentKeyEvent() removes the current key event from
+ * mCurrentKeyEvents.
+ */
+ void RemoveCurrentKeyEvent() {
+ NS_ASSERTION(mCurrentKeyEvents.Length() > 0, "RemoveCurrentKeyEvent() is called unexpectedly");
+ KeyEventState* keyEvent = mCurrentKeyEvents.PopLastElement();
+ if (keyEvent == &mFirstKeyEvent) {
+ keyEvent->Clear();
+ } else {
+ delete keyEvent;
+ }
+ }
+
+ /**
+ * GetCurrentKeyEvent() returns current processing key event.
+ */
+ KeyEventState* GetCurrentKeyEvent() {
+ if (mCurrentKeyEvents.Length() == 0) {
+ return nullptr;
+ }
+ return mCurrentKeyEvents[mCurrentKeyEvents.Length() - 1];
+ }
+
+ struct KeyboardLayoutOverride final {
+ int32_t mKeyboardLayout;
+ bool mOverrideEnabled;
+
+ KeyboardLayoutOverride() : mKeyboardLayout(0), mOverrideEnabled(false) {}
+ };
+
+ const KeyboardLayoutOverride& KeyboardLayoutOverrideRef() const { return mKeyboardOverride; }
+
+ /**
+ * IsPrintableChar() checks whether the unicode character is
+ * a non-printable ASCII character or not. Note that this returns
+ * TRUE even if aChar is a non-printable UNICODE character.
+ *
+ * @param aChar A unicode character.
+ * @return TRUE if aChar is a printable ASCII character
+ * or a unicode character. Otherwise, i.e,
+ * if aChar is a non-printable ASCII character,
+ * FALSE.
+ */
+ static bool IsPrintableChar(char16_t aChar);
+
+ /**
+ * IsNormalCharInputtingEvent() checks whether aNativeEvent causes text input.
+ *
+ * @param aNativeEvent A key event.
+ * @return TRUE if the key event causes text input.
+ * Otherwise, FALSE.
+ */
+ static bool IsNormalCharInputtingEvent(NSEvent* aNativeEvent);
+
+ /**
+ * IsModifierKey() checks whether the native keyCode is for a modifier key.
+ *
+ * @param aNativeKeyCode A native keyCode.
+ * @return TRUE if aNativeKeyCode is for a modifier key.
+ * Otherwise, FALSE.
+ */
+ static bool IsModifierKey(UInt32 aNativeKeyCode);
+
+ private:
+ KeyboardLayoutOverride mKeyboardOverride;
+
+ static int32_t sSecureEventInputCount;
+};
+
+/**
+ * IMEInputHandler manages:
+ * 1. The IME/keyboard layout statement of nsChildView.
+ * 2. The IME composition statement of nsChildView.
+ * And also provides the methods which controls the current IME transaction of
+ * the instance.
+ *
+ * Note that an nsChildView handles one or more NSView's events. E.g., even if
+ * a text editor on XUL panel element, the input events handled on the parent
+ * (or its ancestor) widget handles it (the native focus is set to it). The
+ * actual focused view is notified by OnFocusChangeInGecko.
+ */
+
+class IMEInputHandler : public TextInputHandlerBase {
+ public:
+ // TextEventDispatcherListener methods
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+ NS_IMETHOD_(void) OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) override;
+
+ public:
+ virtual bool OnDestroyWidget(nsChildView* aDestroyingWidget) override;
+
+ virtual void OnFocusChangeInGecko(bool aFocus);
+
+ void OnSelectionChange(const IMENotification& aIMENotification);
+ void OnLayoutChange();
+
+ /**
+ * Call [NSTextInputContext handleEvent] for mouse event support of IME
+ */
+ bool OnHandleEvent(NSEvent* aEvent);
+
+ /**
+ * SetMarkedText() is a handler of setMarkedText of NSTextInput.
+ *
+ * @param aAttrString This mut be an instance of NSAttributedString.
+ * If the aString parameter to
+ * [ChildView setMarkedText:setSelectedRange:]
+ * isn't an instance of NSAttributedString,
+ * create an NSAttributedString from it and pass
+ * that instead.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current marked range.
+ */
+ void SetMarkedText(NSAttributedString* aAttrString, NSRange& aSelectedRange,
+ NSRange* aReplacementRange = nullptr);
+
+ /**
+ * GetAttributedSubstringFromRange() returns an NSAttributedString instance
+ * which is allocated as autorelease for aRange.
+ *
+ * @param aRange The range of string which you want.
+ * @param aActualRange The actual range of the result.
+ * @return The string in aRange. If the string is empty,
+ * this returns nil. If succeeded, this returns
+ * an instance which is allocated as autorelease.
+ * If this has some troubles, returns nil.
+ */
+ NSAttributedString* GetAttributedSubstringFromRange(NSRange& aRange,
+ NSRange* aActualRange = nullptr);
+
+ /**
+ * SelectedRange() returns current selected range.
+ *
+ * @return If an editor has focus, this returns selection
+ * range in the editor. Otherwise, this returns
+ * selection range in the focused document.
+ */
+ NSRange SelectedRange();
+
+ /**
+ * DrawsVerticallyForCharacterAtIndex() returns whether the character at
+ * the given index is being rendered vertically.
+ *
+ * @param aCharIndex The character offset to query.
+ *
+ * @return True if writing-mode is vertical at the given
+ * character offset; otherwise false.
+ */
+ bool DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex);
+
+ /**
+ * FirstRectForCharacterRange() returns first *character* rect in the range.
+ * Cocoa needs the first line rect in the range, but we cannot compute it
+ * on current implementation.
+ *
+ * @param aRange A range of text to examine. Its position is
+ * an offset from the beginning of the focused
+ * editor or document.
+ * @param aActualRange If this is not null, this returns the actual
+ * range used for computing the result.
+ * @return An NSRect containing the first character in
+ * aRange, in screen coordinates.
+ * If the length of aRange is 0, the width will
+ * be 0.
+ */
+ NSRect FirstRectForCharacterRange(NSRange& aRange, NSRange* aActualRange = nullptr);
+
+ /**
+ * CharacterIndexForPoint() returns an offset of a character at aPoint.
+ * XXX This isn't implemented, always returns 0.
+ *
+ * @param The point in screen coordinates.
+ * @return The offset of the character at aPoint from
+ * the beginning of the focused editor or
+ * document.
+ */
+ NSUInteger CharacterIndexForPoint(NSPoint& aPoint);
+
+ /**
+ * GetValidAttributesForMarkedText() returns attributes which we support.
+ *
+ * @return Always empty array for now.
+ */
+ NSArray* GetValidAttributesForMarkedText();
+
+ bool HasMarkedText();
+ NSRange MarkedRange();
+
+ bool IsIMEComposing() { return mIsIMEComposing; }
+ bool IsDeadKeyComposing() { return mIsDeadKeyComposing; }
+ bool IsIMEOpened();
+ bool IsIMEEnabled() { return mIsIMEEnabled; }
+ bool IsASCIICapableOnly() { return mIsASCIICapableOnly; }
+ bool IsEditableContent() const { return mIsIMEEnabled || mIsASCIICapableOnly; }
+ bool IgnoreIMECommit() { return mIgnoreIMECommit; }
+
+ void CommitIMEComposition();
+ void CancelIMEComposition();
+
+ void EnableIME(bool aEnableIME);
+ void SetIMEOpenState(bool aOpen);
+ void SetASCIICapableOnly(bool aASCIICapableOnly);
+
+ /**
+ * True if OSX believes that our view has keyboard focus.
+ */
+ bool IsFocused();
+
+ static CFArrayRef CreateAllIMEModeList();
+ static void DebugPrintAllIMEModes();
+
+ // Don't use ::TSMGetActiveDocument() API directly, the document may not
+ // be what you want.
+ static TSMDocumentID GetCurrentTSMDocumentID();
+
+ protected:
+ // We cannot do some jobs in the given stack by some reasons.
+ // Following flags and the timer provide the execution pending mechanism,
+ // See the comment in nsCocoaTextInputHandler.mm.
+ nsCOMPtr<nsITimer> mTimer;
+ enum { kNotifyIMEOfFocusChangeInGecko = 1, kSyncASCIICapableOnly = 2 };
+ uint32_t mPendingMethods;
+
+ IMEInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~IMEInputHandler();
+
+ void ResetTimer();
+
+ virtual void ExecutePendingMethods();
+
+ /**
+ * InsertTextAsCommittingComposition() commits current composition. If there
+ * is no composition, this starts a composition and commits it immediately.
+ *
+ * @param aAttrString A string which is committed.
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current selection.
+ */
+ void InsertTextAsCommittingComposition(NSAttributedString* aAttrString,
+ NSRange* aReplacementRange);
+
+ /**
+ * MaybeDispatchCurrentKeydownEvent() dispatches eKeyDown event for current
+ * key event. If eKeyDown for current key event has already been dispatched,
+ * this does nothing.
+ *
+ * @param aIsProcessedByIME true if current key event is handled by IME.
+ * @return true if the caller can continue to handle
+ * current key event. Otherwise, false. E.g.,
+ * focus is moved, the widget has been destroyed
+ * or something.
+ */
+ bool MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME);
+
+ private:
+ // If mIsIMEComposing is true, the composition string is stored here.
+ NSString* mIMECompositionString;
+ // If mIsIMEComposing is true, the start offset of the composition string.
+ uint32_t mIMECompositionStart;
+
+ NSRange mMarkedRange;
+ NSRange mSelectedRange;
+
+ NSRange mRangeForWritingMode; // range within which mWritingMode applies
+ mozilla::WritingMode mWritingMode;
+
+ bool mIsIMEComposing;
+ // If the composition started with dead key, mIsDeadKeyComposing is set to
+ // true.
+ bool mIsDeadKeyComposing;
+ bool mIsIMEEnabled;
+ bool mIsASCIICapableOnly;
+ bool mIgnoreIMECommit;
+ bool mIMEHasFocus;
+
+ void KillIMEComposition();
+ void SendCommittedText(NSString* aString);
+ void OpenSystemPreferredLanguageIME();
+
+ // Pending methods
+ void NotifyIMEOfFocusChangeInGecko();
+ void SyncASCIICapableOnly();
+
+ static bool sStaticMembersInitialized;
+ static CFStringRef sLatestIMEOpenedModeInputSourceID;
+ static void InitStaticMembers();
+ static void OnCurrentTextInputSourceChange(CFNotificationCenterRef aCenter, void* aObserver,
+ CFStringRef aName, const void* aObject,
+ CFDictionaryRef aUserInfo);
+
+ static void FlushPendingMethods(nsITimer* aTimer, void* aClosure);
+
+ /**
+ * ConvertToTextRangeStyle converts the given native underline style to
+ * our defined text range type.
+ *
+ * @param aUnderlineStyle NSUnderlineStyleSingle or
+ * NSUnderlineStyleThick.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @return NS_TEXTRANGE_*.
+ */
+ TextRangeType ConvertToTextRangeType(uint32_t aUnderlineStyle, NSRange& aSelectedRange);
+
+ /**
+ * GetRangeCount() computes the range count of aAttrString.
+ *
+ * @param aAttrString An NSAttributedString instance whose number of
+ * NSUnderlineStyleAttributeName ranges you with
+ * to know.
+ * @return The count of NSUnderlineStyleAttributeName
+ * ranges in aAttrString.
+ */
+ uint32_t GetRangeCount(NSAttributedString* aString);
+
+ /**
+ * CreateTextRangeArray() returns text ranges for clauses and/or caret.
+ *
+ * @param aAttrString An NSAttributedString instance which indicates
+ * current composition string.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @return The result is set to the
+ * NSUnderlineStyleAttributeName ranges in
+ * aAttrString.
+ */
+ already_AddRefed<mozilla::TextRangeArray> CreateTextRangeArray(NSAttributedString* aAttrString,
+ NSRange& aSelectedRange);
+
+ /**
+ * DispatchCompositionStartEvent() dispatches a compositionstart event and
+ * initializes the members indicating composition state.
+ *
+ * @return true if it can continues handling composition.
+ * Otherwise, e.g., canceled by the web page,
+ * this returns false.
+ */
+ bool DispatchCompositionStartEvent();
+
+ /**
+ * DispatchCompositionChangeEvent() dispatches a compositionchange event on
+ * mWidget and modifies the members indicating composition state.
+ *
+ * @param aText User text input.
+ * @param aAttrString An NSAttributedString instance which indicates
+ * current composition string.
+ * @param aSelectedRange Current selected range (or caret position).
+ *
+ * @return true if it can continues handling composition.
+ * Otherwise, e.g., canceled by the web page,
+ * this returns false.
+ */
+ bool DispatchCompositionChangeEvent(const nsString& aText, NSAttributedString* aAttrString,
+ NSRange& aSelectedRange);
+
+ /**
+ * DispatchCompositionCommitEvent() dispatches a compositioncommit event or
+ * compositioncommitasis event. If aCommitString is null, dispatches
+ * compositioncommitasis event. I.e., if aCommitString is null, this
+ * commits the composition with the last data. Otherwise, commits the
+ * composition with aCommitString value.
+ *
+ * @return true if the widget isn't destroyed.
+ * Otherwise, false.
+ */
+ bool DispatchCompositionCommitEvent(const nsAString* aCommitString = nullptr);
+
+ // The focused IME handler. Please note that the handler might lost the
+ // actual focus by deactivating the application. If we are active, this
+ // must have the actual focused handle.
+ // We cannot access to the NSInputManager during we aren't active, so, the
+ // focused handler can have an IME transaction even if we are deactive.
+ static IMEInputHandler* sFocusedIMEHandler;
+
+ static bool sCachedIsForRTLLangage;
+};
+
+/**
+ * TextInputHandler implements the NSTextInput protocol.
+ */
+class TextInputHandler : public IMEInputHandler {
+ public:
+ static NSUInteger sLastModifierState;
+
+ static CFArrayRef CreateAllKeyboardLayoutList();
+ static void DebugPrintAllKeyboardLayouts();
+
+ TextInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~TextInputHandler();
+
+ /**
+ * KeyDown event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeKeyDown event.
+ * @param aUniqueId A unique ID for the event.
+ * @return TRUE if the event is dispatched to web
+ * contents or chrome contents. Otherwise, FALSE.
+ */
+ bool HandleKeyDownEvent(NSEvent* aNativeEvent, uint32_t aUniqueId);
+
+ /**
+ * KeyUp event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeKeyUp event.
+ */
+ void HandleKeyUpEvent(NSEvent* aNativeEvent);
+
+ /**
+ * FlagsChanged event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeFlagsChanged event.
+ */
+ void HandleFlagsChanged(NSEvent* aNativeEvent);
+
+ /**
+ * Insert the string to content. I.e., this is a text input event handler.
+ * If this is called during keydown event handling, this may dispatch a
+ * eKeyPress event. If this is called during composition, this commits
+ * the composition by the aAttrString.
+ *
+ * @param aAttrString An inserted string.
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current selection.
+ */
+ void InsertText(NSAttributedString* aAttrString, NSRange* aReplacementRange = nullptr);
+
+ /**
+ * Handles aCommand. This may cause dispatching an eKeyPress event.
+ *
+ * @param aCommand The command which receives from Cocoa.
+ * @return true if this handles the command even if it does
+ * nothing actually. Otherwise, false.
+ */
+ bool HandleCommand(Command aCommand);
+
+ /**
+ * doCommandBySelector event handler.
+ *
+ * @param aSelector A selector of the command.
+ * @return TRUE if the command is consumed. Otherwise,
+ * FALSE.
+ */
+ bool DoCommandBySelector(const char* aSelector);
+
+ /**
+ * KeyPressWasHandled() checks whether keypress event was handled or not.
+ *
+ * @return TRUE if keypress event for latest native key
+ * event was handled. Otherwise, FALSE.
+ * If this handler isn't handling any key events,
+ * always returns FALSE.
+ */
+ bool KeyPressWasHandled() {
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ return currentKeyEvent && currentKeyEvent->mKeyPressHandled;
+ }
+
+ protected:
+ // Stores the association of device dependent modifier flags with a modifier
+ // keyCode. Being device dependent, this association may differ from one kind
+ // of hardware to the next.
+ struct ModifierKey {
+ NSUInteger flags;
+ unsigned short keyCode;
+
+ ModifierKey(NSUInteger aFlags, unsigned short aKeyCode) : flags(aFlags), keyCode(aKeyCode) {}
+
+ NSUInteger GetDeviceDependentFlags() const {
+ return (flags & ~NSEventModifierFlagDeviceIndependentFlagsMask);
+ }
+
+ NSUInteger GetDeviceIndependentFlags() const {
+ return (flags & NSEventModifierFlagDeviceIndependentFlagsMask);
+ }
+ };
+ typedef nsTArray<ModifierKey> ModifierKeyArray;
+ ModifierKeyArray mModifierKeys;
+
+ /**
+ * GetModifierKeyForNativeKeyCode() returns the stored ModifierKey for
+ * the key.
+ */
+ const ModifierKey* GetModifierKeyForNativeKeyCode(unsigned short aKeyCode) const;
+
+ /**
+ * GetModifierKeyForDeviceDependentFlags() returns the stored ModifierKey for
+ * the device dependent flags.
+ */
+ const ModifierKey* GetModifierKeyForDeviceDependentFlags(NSUInteger aFlags) const;
+
+ /**
+ * DispatchKeyEventForFlagsChanged() dispatches keydown event or keyup event
+ * for the aNativeEvent.
+ *
+ * @param aNativeEvent A native flagschanged event which you want to
+ * dispatch our key event for.
+ * @param aDispatchKeyDown TRUE if you want to dispatch a keydown event.
+ * Otherwise, i.e., to dispatch keyup event,
+ * FALSE.
+ */
+ void DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent, bool aDispatchKeyDown);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // TextInputHandler_h_
diff --git a/widget/cocoa/TextInputHandler.mm b/widget/cocoa/TextInputHandler.mm
new file mode 100644
index 0000000000..a6d9dd3c02
--- /dev/null
+++ b/widget/cocoa/TextInputHandler.mm
@@ -0,0 +1,5073 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* 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/. */
+
+#include "TextInputHandler.h"
+
+#include "mozilla/Logging.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+
+#include "nsChildView.h"
+#include "nsCocoaFeatures.h"
+#include "nsObjCExceptions.h"
+#include "nsBidiUtils.h"
+#include "nsToolkit.h"
+#include "nsCocoaUtils.h"
+#include "WidgetUtils.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+// For collecting other people's log, tell them `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gIMELog("IMEHandler");
+
+// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync`
+// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gKeyLog("KeyboardHandler");
+
+// The behavior of `TextInputHandler` class is important both for logging
+// keyboard handler and IME handler. Therefore, the behavior is logged when
+// either `IMEHandler` or `KeyboardHandler` is set to `MOZ_LOG`. Therefore,
+// you may not need to tell people `MOZ_LOG=IMEHandler:4,KeyboardHandler:4,sync`.
+#define MOZ_LOG_KEY_OR_IME(aLogLevel, aArgs) \
+ MOZ_LOG(MOZ_LOG_TEST(gIMELog, aLogLevel) ? gIMELog : gKeyLog, aLogLevel, aArgs)
+
+static const char* OnOrOff(bool aBool) { return aBool ? "ON" : "off"; }
+
+static const char* TrueOrFalse(bool aBool) { return aBool ? "TRUE" : "FALSE"; }
+
+static const char* GetKeyNameForNativeKeyCode(unsigned short aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_Escape:
+ return "Escape";
+ case kVK_RightCommand:
+ return "Right-Command";
+ case kVK_Command:
+ return "Command";
+ case kVK_Shift:
+ return "Shift";
+ case kVK_CapsLock:
+ return "CapsLock";
+ case kVK_Option:
+ return "Option";
+ case kVK_Control:
+ return "Control";
+ case kVK_RightShift:
+ return "Right-Shift";
+ case kVK_RightOption:
+ return "Right-Option";
+ case kVK_RightControl:
+ return "Right-Control";
+ case kVK_ANSI_KeypadClear:
+ return "Clear";
+
+ case kVK_F1:
+ return "F1";
+ case kVK_F2:
+ return "F2";
+ case kVK_F3:
+ return "F3";
+ case kVK_F4:
+ return "F4";
+ case kVK_F5:
+ return "F5";
+ case kVK_F6:
+ return "F6";
+ case kVK_F7:
+ return "F7";
+ case kVK_F8:
+ return "F8";
+ case kVK_F9:
+ return "F9";
+ case kVK_F10:
+ return "F10";
+ case kVK_F11:
+ return "F11";
+ case kVK_F12:
+ return "F12";
+ case kVK_F13:
+ return "F13/PrintScreen";
+ case kVK_F14:
+ return "F14/ScrollLock";
+ case kVK_F15:
+ return "F15/Pause";
+
+ case kVK_ANSI_Keypad0:
+ return "NumPad-0";
+ case kVK_ANSI_Keypad1:
+ return "NumPad-1";
+ case kVK_ANSI_Keypad2:
+ return "NumPad-2";
+ case kVK_ANSI_Keypad3:
+ return "NumPad-3";
+ case kVK_ANSI_Keypad4:
+ return "NumPad-4";
+ case kVK_ANSI_Keypad5:
+ return "NumPad-5";
+ case kVK_ANSI_Keypad6:
+ return "NumPad-6";
+ case kVK_ANSI_Keypad7:
+ return "NumPad-7";
+ case kVK_ANSI_Keypad8:
+ return "NumPad-8";
+ case kVK_ANSI_Keypad9:
+ return "NumPad-9";
+
+ case kVK_ANSI_KeypadMultiply:
+ return "NumPad-*";
+ case kVK_ANSI_KeypadPlus:
+ return "NumPad-+";
+ case kVK_ANSI_KeypadMinus:
+ return "NumPad--";
+ case kVK_ANSI_KeypadDecimal:
+ return "NumPad-.";
+ case kVK_ANSI_KeypadDivide:
+ return "NumPad-/";
+ case kVK_ANSI_KeypadEquals:
+ return "NumPad-=";
+ case kVK_ANSI_KeypadEnter:
+ return "NumPad-Enter";
+ case kVK_Return:
+ return "Return";
+ case kVK_Powerbook_KeypadEnter:
+ return "NumPad-EnterOnPowerBook";
+
+ case kVK_PC_Insert:
+ return "Insert/Help";
+ case kVK_PC_Delete:
+ return "Delete";
+ case kVK_Tab:
+ return "Tab";
+ case kVK_PC_Backspace:
+ return "Backspace";
+ case kVK_Home:
+ return "Home";
+ case kVK_End:
+ return "End";
+ case kVK_PageUp:
+ return "PageUp";
+ case kVK_PageDown:
+ return "PageDown";
+ case kVK_LeftArrow:
+ return "LeftArrow";
+ case kVK_RightArrow:
+ return "RightArrow";
+ case kVK_UpArrow:
+ return "UpArrow";
+ case kVK_DownArrow:
+ return "DownArrow";
+ case kVK_PC_ContextMenu:
+ return "ContextMenu";
+
+ case kVK_Function:
+ return "Function";
+ case kVK_VolumeUp:
+ return "VolumeUp";
+ case kVK_VolumeDown:
+ return "VolumeDown";
+ case kVK_Mute:
+ return "Mute";
+
+ case kVK_ISO_Section:
+ return "ISO_Section";
+
+ case kVK_JIS_Yen:
+ return "JIS_Yen";
+ case kVK_JIS_Underscore:
+ return "JIS_Underscore";
+ case kVK_JIS_KeypadComma:
+ return "JIS_KeypadComma";
+ case kVK_JIS_Eisu:
+ return "JIS_Eisu";
+ case kVK_JIS_Kana:
+ return "JIS_Kana";
+
+ case kVK_ANSI_A:
+ return "A";
+ case kVK_ANSI_B:
+ return "B";
+ case kVK_ANSI_C:
+ return "C";
+ case kVK_ANSI_D:
+ return "D";
+ case kVK_ANSI_E:
+ return "E";
+ case kVK_ANSI_F:
+ return "F";
+ case kVK_ANSI_G:
+ return "G";
+ case kVK_ANSI_H:
+ return "H";
+ case kVK_ANSI_I:
+ return "I";
+ case kVK_ANSI_J:
+ return "J";
+ case kVK_ANSI_K:
+ return "K";
+ case kVK_ANSI_L:
+ return "L";
+ case kVK_ANSI_M:
+ return "M";
+ case kVK_ANSI_N:
+ return "N";
+ case kVK_ANSI_O:
+ return "O";
+ case kVK_ANSI_P:
+ return "P";
+ case kVK_ANSI_Q:
+ return "Q";
+ case kVK_ANSI_R:
+ return "R";
+ case kVK_ANSI_S:
+ return "S";
+ case kVK_ANSI_T:
+ return "T";
+ case kVK_ANSI_U:
+ return "U";
+ case kVK_ANSI_V:
+ return "V";
+ case kVK_ANSI_W:
+ return "W";
+ case kVK_ANSI_X:
+ return "X";
+ case kVK_ANSI_Y:
+ return "Y";
+ case kVK_ANSI_Z:
+ return "Z";
+
+ case kVK_ANSI_1:
+ return "1";
+ case kVK_ANSI_2:
+ return "2";
+ case kVK_ANSI_3:
+ return "3";
+ case kVK_ANSI_4:
+ return "4";
+ case kVK_ANSI_5:
+ return "5";
+ case kVK_ANSI_6:
+ return "6";
+ case kVK_ANSI_7:
+ return "7";
+ case kVK_ANSI_8:
+ return "8";
+ case kVK_ANSI_9:
+ return "9";
+ case kVK_ANSI_0:
+ return "0";
+ case kVK_ANSI_Equal:
+ return "Equal";
+ case kVK_ANSI_Minus:
+ return "Minus";
+ case kVK_ANSI_RightBracket:
+ return "RightBracket";
+ case kVK_ANSI_LeftBracket:
+ return "LeftBracket";
+ case kVK_ANSI_Quote:
+ return "Quote";
+ case kVK_ANSI_Semicolon:
+ return "Semicolon";
+ case kVK_ANSI_Backslash:
+ return "Backslash";
+ case kVK_ANSI_Comma:
+ return "Comma";
+ case kVK_ANSI_Slash:
+ return "Slash";
+ case kVK_ANSI_Period:
+ return "Period";
+ case kVK_ANSI_Grave:
+ return "Grave";
+
+ default:
+ return "undefined";
+ }
+}
+
+static const char* GetCharacters(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return "";
+ }
+ nsAutoString escapedStr;
+ for (uint32_t i = 0; i < aString.Length(); i++) {
+ char16_t ch = aString.CharAt(i);
+ if (ch < 0x20) {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ } else if (ch <= 0x7E) {
+ escapedStr += ch;
+ } else {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += ch;
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ }
+ }
+
+ // the result will be freed automatically by cocoa.
+ NSString* result = nsCocoaUtils::ToNSString(escapedStr);
+ return [result UTF8String];
+}
+
+static const char* GetCharacters(const NSString* aString) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(aString, str);
+ return GetCharacters(str);
+}
+
+static const char* GetCharacters(const CFStringRef aString) {
+ const NSString* str = reinterpret_cast<const NSString*>(aString);
+ return GetCharacters(str);
+}
+
+static const char* GetNativeKeyEventType(NSEvent* aNativeEvent) {
+ switch ([aNativeEvent type]) {
+ case NSEventTypeKeyDown:
+ return "NSEventTypeKeyDown";
+ case NSEventTypeKeyUp:
+ return "NSEventTypeKeyUp";
+ case NSEventTypeFlagsChanged:
+ return "NSEventTypeFlagsChanged";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetGeckoKeyEventType(const WidgetEvent& aEvent) {
+ switch (aEvent.mMessage) {
+ case eKeyDown:
+ return "eKeyDown";
+ case eKeyUp:
+ return "eKeyUp";
+ case eKeyPress:
+ return "eKeyPress";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetWindowLevelName(NSInteger aWindowLevel) {
+ switch (aWindowLevel) {
+ case kCGBaseWindowLevelKey:
+ return "kCGBaseWindowLevelKey (NSNormalWindowLevel)";
+ case kCGMinimumWindowLevelKey:
+ return "kCGMinimumWindowLevelKey";
+ case kCGDesktopWindowLevelKey:
+ return "kCGDesktopWindowLevelKey";
+ case kCGBackstopMenuLevelKey:
+ return "kCGBackstopMenuLevelKey";
+ case kCGNormalWindowLevelKey:
+ return "kCGNormalWindowLevelKey";
+ case kCGFloatingWindowLevelKey:
+ return "kCGFloatingWindowLevelKey (NSFloatingWindowLevel)";
+ case kCGTornOffMenuWindowLevelKey:
+ return "kCGTornOffMenuWindowLevelKey (NSSubmenuWindowLevel, NSTornOffMenuWindowLevel)";
+ case kCGDockWindowLevelKey:
+ return "kCGDockWindowLevelKey (NSDockWindowLevel)";
+ case kCGMainMenuWindowLevelKey:
+ return "kCGMainMenuWindowLevelKey (NSMainMenuWindowLevel)";
+ case kCGStatusWindowLevelKey:
+ return "kCGStatusWindowLevelKey (NSStatusWindowLevel)";
+ case kCGModalPanelWindowLevelKey:
+ return "kCGModalPanelWindowLevelKey (NSModalPanelWindowLevel)";
+ case kCGPopUpMenuWindowLevelKey:
+ return "kCGPopUpMenuWindowLevelKey (NSPopUpMenuWindowLevel)";
+ case kCGDraggingWindowLevelKey:
+ return "kCGDraggingWindowLevelKey";
+ case kCGScreenSaverWindowLevelKey:
+ return "kCGScreenSaverWindowLevelKey (NSScreenSaverWindowLevel)";
+ case kCGMaximumWindowLevelKey:
+ return "kCGMaximumWindowLevelKey";
+ case kCGOverlayWindowLevelKey:
+ return "kCGOverlayWindowLevelKey";
+ case kCGHelpWindowLevelKey:
+ return "kCGHelpWindowLevelKey";
+ case kCGUtilityWindowLevelKey:
+ return "kCGUtilityWindowLevelKey";
+ case kCGDesktopIconWindowLevelKey:
+ return "kCGDesktopIconWindowLevelKey";
+ case kCGCursorWindowLevelKey:
+ return "kCGCursorWindowLevelKey";
+ case kCGNumberOfWindowLevelKeys:
+ return "kCGNumberOfWindowLevelKeys";
+ default:
+ return "unknown window level";
+ }
+}
+
+static bool IsControlChar(uint32_t aCharCode) { return aCharCode < ' ' || aCharCode == 0x7F; }
+
+static uint32_t gHandlerInstanceCount = 0;
+
+static void EnsureToLogAllKeyboardLayoutsAndIMEs() {
+ static bool sDone = false;
+ if (!sDone) {
+ sDone = true;
+ TextInputHandler::DebugPrintAllKeyboardLayouts();
+ IMEInputHandler::DebugPrintAllIMEModes();
+ }
+}
+
+inline NSRange MakeNSRangeFrom(const Maybe<OffsetAndData<uint32_t>>& aOffsetAndData) {
+ if (aOffsetAndData.isNothing()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return NSMakeRange(aOffsetAndData->StartOffset(), aOffsetAndData->Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TISInputSourceWrapper implementation
+ *
+ ******************************************************************************/
+
+TISInputSourceWrapper* TISInputSourceWrapper::sCurrentInputSource = nullptr;
+
+// static
+TISInputSourceWrapper& TISInputSourceWrapper::CurrentInputSource() {
+ if (!sCurrentInputSource) {
+ sCurrentInputSource = new TISInputSourceWrapper();
+ }
+ if (!sCurrentInputSource->IsInitializedByCurrentInputSource()) {
+ sCurrentInputSource->InitByCurrentInputSource();
+ }
+ return *sCurrentInputSource;
+}
+
+// static
+void TISInputSourceWrapper::Shutdown() {
+ if (!sCurrentInputSource) {
+ return;
+ }
+ sCurrentInputSource->Clear();
+ delete sCurrentInputSource;
+ sCurrentInputSource = nullptr;
+}
+
+bool TISInputSourceWrapper::TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType,
+ nsAString& aStr) {
+ aStr.Truncate();
+
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey),
+ OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey),
+ OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ NS_ENSURE_TRUE(UCKey, false);
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType,
+ kUCKeyTranslateNoDeadKeysMask, &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%zu", this,
+ static_cast<int>(err), len));
+
+ NS_ENSURE_TRUE(err == noErr, false);
+ if (len == 0) {
+ return true;
+ }
+ if (!aStr.SetLength(len, fallible)) {
+ return false;
+ }
+ NS_ASSERTION(sizeof(char16_t) == sizeof(UniChar),
+ "size of char16_t and size of UniChar are different");
+ memcpy(aStr.BeginWriting(), chars, len * sizeof(char16_t));
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aStr=\"%s\"", this,
+ NS_ConvertUTF16toUTF8(aStr).get()));
+
+ return true;
+}
+
+uint32_t TISInputSourceWrapper::TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers,
+ UInt32 aKbType) {
+ nsAutoString str;
+ if (!TranslateToString(aKeyCode, aModifiers, aKbType, str) || str.Length() != 1) {
+ return 0;
+ }
+ return static_cast<uint32_t>(str.CharAt(0));
+}
+
+bool TISInputSourceWrapper::IsDeadKey(NSEvent* aNativeKeyEvent) {
+ if ([[aNativeKeyEvent characters] length]) {
+ return false;
+ }
+
+ // Assmue that if control key or command key is pressed, it's not a dead key.
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ if (cocoaState & (NSEventModifierFlagControl | NSEventModifierFlagCommand)) {
+ return false;
+ }
+
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ switch (nativeKeyCode) {
+ case kVK_ANSI_A:
+ case kVK_ANSI_B:
+ case kVK_ANSI_C:
+ case kVK_ANSI_D:
+ case kVK_ANSI_E:
+ case kVK_ANSI_F:
+ case kVK_ANSI_G:
+ case kVK_ANSI_H:
+ case kVK_ANSI_I:
+ case kVK_ANSI_J:
+ case kVK_ANSI_K:
+ case kVK_ANSI_L:
+ case kVK_ANSI_M:
+ case kVK_ANSI_N:
+ case kVK_ANSI_O:
+ case kVK_ANSI_P:
+ case kVK_ANSI_Q:
+ case kVK_ANSI_R:
+ case kVK_ANSI_S:
+ case kVK_ANSI_T:
+ case kVK_ANSI_U:
+ case kVK_ANSI_V:
+ case kVK_ANSI_W:
+ case kVK_ANSI_X:
+ case kVK_ANSI_Y:
+ case kVK_ANSI_Z:
+ case kVK_ANSI_1:
+ case kVK_ANSI_2:
+ case kVK_ANSI_3:
+ case kVK_ANSI_4:
+ case kVK_ANSI_5:
+ case kVK_ANSI_6:
+ case kVK_ANSI_7:
+ case kVK_ANSI_8:
+ case kVK_ANSI_9:
+ case kVK_ANSI_0:
+ case kVK_ANSI_Equal:
+ case kVK_ANSI_Minus:
+ case kVK_ANSI_RightBracket:
+ case kVK_ANSI_LeftBracket:
+ case kVK_ANSI_Quote:
+ case kVK_ANSI_Semicolon:
+ case kVK_ANSI_Backslash:
+ case kVK_ANSI_Comma:
+ case kVK_ANSI_Slash:
+ case kVK_ANSI_Period:
+ case kVK_ANSI_Grave:
+ case kVK_JIS_Yen:
+ case kVK_JIS_Underscore:
+ break;
+ default:
+ // Let's assume that dead key can be only a printable key in standard
+ // position.
+ return false;
+ }
+
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ UInt32 kbType = GetKbdType();
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ return IsDeadKey(nativeKeyCode, carbonState, kbType);
+}
+
+bool TISInputSourceWrapper::IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType) {
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey),
+ OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey),
+ OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ if (NS_WARN_IF(!UCKey)) {
+ return false;
+ }
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType, 0,
+ &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, err=0x%X, "
+ "len=%zu, deadKeyState=%u",
+ this, static_cast<int>(err), len, deadKeyState));
+
+ if (NS_WARN_IF(err != noErr)) {
+ return false;
+ }
+
+ return deadKeyState != 0;
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const char* aID) {
+ Clear();
+ if (!aID) return;
+
+ CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID, kCFStringEncodingASCII);
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const nsString& aID) {
+ Clear();
+ if (aID.IsEmpty()) return;
+ CFStringRef idstr = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aID.get()), aID.Length());
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const CFStringRef aID) {
+ Clear();
+ if (!aID) return;
+ const void* keys[] = {kTISPropertyInputSourceID};
+ const void* values[] = {aID};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ mInputSourceList = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ if (::CFArrayGetCount(mInputSourceList) > 0) {
+ mInputSource = static_cast<TISInputSourceRef>(
+ const_cast<void*>(::CFArrayGetValueAtIndex(mInputSourceList, 0)));
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByLayoutID(SInt32 aLayoutID, bool aOverrideKeyboard) {
+ // NOTE: Doument new layout IDs in TextInputHandler.h when you add ones.
+ switch (aLayoutID) {
+ case 0:
+ InitByInputSourceID("com.apple.keylayout.US");
+ break;
+ case 1:
+ InitByInputSourceID("com.apple.keylayout.Greek");
+ break;
+ case 2:
+ InitByInputSourceID("com.apple.keylayout.German");
+ break;
+ case 3:
+ InitByInputSourceID("com.apple.keylayout.Swedish-Pro");
+ break;
+ case 4:
+ InitByInputSourceID("com.apple.keylayout.DVORAK-QWERTYCMD");
+ break;
+ case 5:
+ InitByInputSourceID("com.apple.keylayout.Thai");
+ break;
+ case 6:
+ InitByInputSourceID("com.apple.keylayout.Arabic");
+ break;
+ case 7:
+ InitByInputSourceID("com.apple.keylayout.ArabicPC");
+ break;
+ case 8:
+ InitByInputSourceID("com.apple.keylayout.French");
+ break;
+ case 9:
+ InitByInputSourceID("com.apple.keylayout.Hebrew");
+ break;
+ case 10:
+ InitByInputSourceID("com.apple.keylayout.Lithuanian");
+ break;
+ case 11:
+ InitByInputSourceID("com.apple.keylayout.Norwegian");
+ break;
+ case 12:
+ InitByInputSourceID("com.apple.keylayout.Spanish");
+ break;
+ default:
+ Clear();
+ break;
+ }
+ mOverrideKeyboard = aOverrideKeyboard;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentKeyboardLayoutInputSource();
+ }
+ // If this causes composition, the current keyboard layout may input non-ASCII
+ // characters such as Japanese Kana characters or Hangul characters.
+ // However, we need to set ASCII characters to DOM key events for consistency
+ // with other platforms.
+ if (IsOpenedIMEMode()) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (mKeyboardLayout) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = nullptr;
+ }
+ }
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputMethodKeyboardLayoutOverride() {
+ Clear();
+ mInputSource = ::TISCopyInputMethodKeyboardLayoutOverride();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByTISInputSourceRef(TISInputSourceRef aInputSource) {
+ Clear();
+ mInputSource = aInputSource;
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+void TISInputSourceWrapper::InitByLanguage(CFStringRef aLanguage) {
+ Clear();
+ mInputSource = ::TISCopyInputSourceForLanguage(aLanguage);
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+const UCKeyboardLayout* TISInputSourceWrapper::GetUCKeyboardLayout() {
+ NS_ENSURE_TRUE(mKeyboardLayout, nullptr);
+ if (mUCKeyboardLayout) {
+ return mUCKeyboardLayout;
+ }
+ CFDataRef uchr = static_cast<CFDataRef>(
+ ::TISGetInputSourceProperty(mKeyboardLayout, kTISPropertyUnicodeKeyLayoutData));
+
+ // We should be always able to get the layout here.
+ NS_ENSURE_TRUE(uchr, nullptr);
+ mUCKeyboardLayout = reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(uchr));
+ return mUCKeyboardLayout;
+}
+
+bool TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) {
+ CFBooleanRef ret = static_cast<CFBooleanRef>(::TISGetInputSourceProperty(mInputSource, aKey));
+ return ::CFBooleanGetValue(ret);
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, CFStringRef& aStr) {
+ aStr = static_cast<CFStringRef>(::TISGetInputSourceProperty(mInputSource, aKey));
+ return aStr != nullptr;
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, nsAString& aStr) {
+ CFStringRef str;
+ GetStringProperty(aKey, str);
+ nsCocoaUtils::GetStringForNSString((const NSString*)str, aStr);
+ return !aStr.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsOpenedIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ if (!IsIMEMode()) return false;
+ return !IsASCIICapable();
+}
+
+bool TISInputSourceWrapper::IsIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardInputMode, str, 0) == kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::IsKeyboardLayout() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardLayout, str, 0) == kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::GetLanguageList(CFArrayRef& aLanguageList) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ aLanguageList = static_cast<CFArrayRef>(
+ ::TISGetInputSourceProperty(mInputSource, kTISPropertyInputSourceLanguages));
+ return aLanguageList != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(CFStringRef& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFArrayRef langList;
+ NS_ENSURE_TRUE(GetLanguageList(langList), false);
+ if (::CFArrayGetCount(langList) == 0) return false;
+ aPrimaryLanguage = static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, 0));
+ return aPrimaryLanguage != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(nsAString& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef primaryLanguage;
+ NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), false);
+ nsCocoaUtils::GetStringForNSString((const NSString*)primaryLanguage, aPrimaryLanguage);
+ return !aPrimaryLanguage.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsForRTLLanguage() {
+ if (mIsRTL < 0) {
+ // Get the input character of the 'A' key of ANSI keyboard layout.
+ nsAutoString str;
+ bool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str);
+ NS_ENSURE_TRUE(ret, ret);
+ char16_t ch = str.IsEmpty() ? char16_t(0) : str.CharAt(0);
+ mIsRTL = UTF16_CODE_UNIT_IS_BIDI(ch);
+ }
+ return mIsRTL != 0;
+}
+
+bool TISInputSourceWrapper::IsForJapaneseLanguage() {
+ nsAutoString lang;
+ GetPrimaryLanguage(lang);
+ return lang.EqualsLiteral("ja");
+}
+
+bool TISInputSourceWrapper::IsInitializedByCurrentInputSource() {
+ return mInputSource == ::TISCopyCurrentKeyboardInputSource();
+}
+
+void TISInputSourceWrapper::Select() {
+ if (!mInputSource) return;
+ ::TISSelectInputSource(mInputSource);
+}
+
+void TISInputSourceWrapper::Clear() {
+ // Clear() is always called when TISInputSourceWrappper is created.
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+
+ if (mInputSourceList) {
+ ::CFRelease(mInputSourceList);
+ }
+ mInputSourceList = nullptr;
+ mInputSource = nullptr;
+ mKeyboardLayout = nullptr;
+ mIsRTL = -1;
+ mUCKeyboardLayout = nullptr;
+ mOverrideKeyboard = false;
+}
+
+bool TISInputSourceWrapper::IsPrintableKeyEvent(NSEvent* aNativeKeyEvent) const {
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ bool isPrintableKey = !TextInputHandler::IsSpecialGeckoKey(nativeKeyCode);
+ if (isPrintableKey && [aNativeKeyEvent type] != NSEventTypeKeyDown &&
+ [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ NS_WARNING("Why would a printable key not be an NSEventTypeKeyDown or NSEventTypeKeyUp event?");
+ isPrintableKey = false;
+ }
+ return isPrintableKey;
+}
+
+UInt32 TISInputSourceWrapper::GetKbdType() const {
+ // If a keyboard layout override is set, we also need to force the keyboard
+ // type to something ANSI to avoid test failures on machines with JIS
+ // keyboards (since the pair of keyboard layout and physical keyboard type
+ // form the actual key layout). This assumes that the test setting the
+ // override was written assuming an ANSI keyboard.
+ return mOverrideKeyboard ? eKbdType_ANSI : ::LMGetKbdType();
+}
+
+void TISInputSourceWrapper::ComputeInsertStringForCharCode(NSEvent* aNativeKeyEvent,
+ const WidgetKeyboardEvent& aKeyEvent,
+ const nsAString* aInsertString,
+ nsAString& aResult) {
+ if (aInsertString) {
+ // If the caller expects that the aInsertString will be input, we shouldn't
+ // change it.
+ aResult = *aInsertString;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ // If IME is open, [aNativeKeyEvent characters] may be a character
+ // which will be appended to the composition string. However, especially,
+ // while IME is disabled, most users and developers expect the key event
+ // works as IME closed. So, we should compute the aResult with
+ // the ASCII capable keyboard layout.
+ // NOTE: Such keyboard layouts typically change the layout to its ASCII
+ // capable layout when Command key is pressed. And we don't worry
+ // when Control key is pressed too because it causes inputting
+ // control characters.
+ // Additionally, if the key event doesn't input any text, the event may be
+ // dead key event. In this case, the charCode value should be the dead
+ // character.
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ if ((!aKeyEvent.IsMeta() && !aKeyEvent.IsControl() && IsOpenedIMEMode()) ||
+ ![[aNativeKeyEvent characters] length]) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]);
+ uint32_t ch = TranslateToChar(nativeKeyCode, state, GetKbdType());
+ if (ch) {
+ aResult = ch;
+ }
+ } else {
+ // If the caller isn't sure what string will be input, let's use
+ // characters of NSEvent.
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aResult);
+ }
+
+ // If control key is pressed and the eventChars is a non-printable control
+ // character, we should convert it to ASCII alphabet.
+ if (aKeyEvent.IsControl() && !aResult.IsEmpty() && aResult[0] <= char16_t(26)) {
+ aResult = (aKeyEvent.IsShift() ^ aKeyEvent.IsCapsLocked())
+ ? static_cast<char16_t>(aResult[0] + ('A' - 1))
+ : static_cast<char16_t>(aResult[0] + ('a' - 1));
+ }
+ // If Meta key is pressed, it may cause to switch the keyboard layout like
+ // Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ else if (aKeyEvent.IsMeta() && !(aKeyEvent.IsControl() || aKeyEvent.IsAlt())) {
+ UInt32 kbType = GetKbdType();
+ UInt32 numLockState = aKeyEvent.IsNumLocked() ? kEventKeyModifierNumLockMask : 0;
+ UInt32 capsLockState = aKeyEvent.IsCapsLocked() ? alphaLock : 0;
+ UInt32 shiftState = aKeyEvent.IsShift() ? shiftKey : 0;
+ uint32_t uncmdedChar = TranslateToChar(nativeKeyCode, numLockState, kbType);
+ uint32_t cmdedChar = TranslateToChar(nativeKeyCode, cmdKey | numLockState, kbType);
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock.
+ uint32_t ch = 0;
+ if (uncmdedChar == cmdedChar) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ ch = TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState, kbType);
+ } else {
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ uint32_t uncmdedUSChar = USLayout.TranslateToChar(nativeKeyCode, numLockState, kbType);
+ // If it looks like characters from US keyboard layout when Command key
+ // is pressed, we should compute a character in the layout.
+ if (uncmdedUSChar == cmdedChar) {
+ ch = USLayout.TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState,
+ kbType);
+ }
+ }
+
+ // If there is a more preferred character for the commanded key event,
+ // we should use it.
+ if (ch) {
+ aResult = ch;
+ }
+ }
+ }
+
+ // Remove control characters which shouldn't be inputted on editor.
+ // XXX Currently, we don't find any cases inserting control characters with
+ // printable character. So, just checking first character is enough.
+ if (!aResult.IsEmpty() && IsControlChar(aResult[0])) {
+ aResult.Truncate();
+ }
+}
+
+void TISInputSourceWrapper::InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(!aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress,
+ "eKeyPress event should not be marked as proccessed by IME");
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, aNativeKeyEvent=%p, "
+ "aKeyEvent.mMessage=%s, aProcessedByIME=%s, aInsertString=%p, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, GetGeckoKeyEventType(aKeyEvent), TrueOrFalse(aIsProcessedByIME),
+ aInsertString, TrueOrFalse(IsOpenedIMEMode())));
+
+ if (NS_WARN_IF(!aNativeKeyEvent)) {
+ return;
+ }
+
+ nsCocoaUtils::InitInputEvent(aKeyEvent, aNativeKeyEvent);
+
+ // This is used only while dispatching the event (which is a synchronous
+ // call), so there is no need to retain and release this data.
+ aKeyEvent.mNativeKeyEvent = aNativeKeyEvent;
+
+ aKeyEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+
+ UInt32 kbType = GetKbdType();
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ // macOS handles dead key as IME. If the key is first key press of dead
+ // key, we should use KEY_NAME_INDEX_Dead for first (dead) key event.
+ // So, if aIsProcessedByIME is true, it may be dead key. Let's check
+ // if current key event is a dead key's keydown event.
+ bool isProcessedByIME =
+ aIsProcessedByIME && !TISInputSourceWrapper::CurrentInputSource().IsDeadKey(aNativeKeyEvent);
+
+ aKeyEvent.mKeyCode = isProcessedByIME
+ ? NS_VK_PROCESSKEY
+ : ComputeGeckoKeyCode(nativeKeyCode, kbType, aKeyEvent.IsMeta());
+
+ switch (nativeKeyCode) {
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ aKeyEvent.mLocation = eKeyLocationLeft;
+ break;
+
+ case kVK_RightCommand:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ aKeyEvent.mLocation = eKeyLocationRight;
+ break;
+
+ case kVK_ANSI_Keypad0:
+ case kVK_ANSI_Keypad1:
+ case kVK_ANSI_Keypad2:
+ case kVK_ANSI_Keypad3:
+ case kVK_ANSI_Keypad4:
+ case kVK_ANSI_Keypad5:
+ case kVK_ANSI_Keypad6:
+ case kVK_ANSI_Keypad7:
+ case kVK_ANSI_Keypad8:
+ case kVK_ANSI_Keypad9:
+ case kVK_ANSI_KeypadMultiply:
+ case kVK_ANSI_KeypadPlus:
+ case kVK_ANSI_KeypadMinus:
+ case kVK_ANSI_KeypadDecimal:
+ case kVK_ANSI_KeypadDivide:
+ case kVK_ANSI_KeypadEquals:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_JIS_KeypadComma:
+ case kVK_Powerbook_KeypadEnter:
+ aKeyEvent.mLocation = eKeyLocationNumpad;
+ break;
+
+ default:
+ aKeyEvent.mLocation = eKeyLocationStandard;
+ break;
+ }
+
+ aKeyEvent.mIsRepeat =
+ ([aNativeKeyEvent type] == NSEventTypeKeyDown) ? [aNativeKeyEvent isARepeat] : false;
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, "
+ "shift=%s, ctrl=%s, alt=%s, meta=%s",
+ this, OnOrOff(aKeyEvent.IsShift()), OnOrOff(aKeyEvent.IsControl()),
+ OnOrOff(aKeyEvent.IsAlt()), OnOrOff(aKeyEvent.IsMeta())));
+
+ if (isProcessedByIME) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ // If insertText calls this method, let's use the string.
+ if (aInsertString && !aInsertString->IsEmpty() && !IsControlChar((*aInsertString)[0])) {
+ aKeyEvent.mKeyValue = *aInsertString;
+ }
+ // If meta key is pressed, the printable key layout may be switched from
+ // non-ASCII capable layout to ASCII capable, or from Dvorak to QWERTY.
+ // KeyboardEvent.key value should be the switched layout's character.
+ else if (aKeyEvent.IsMeta()) {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aKeyEvent.mKeyValue);
+ }
+ // If control key is pressed, some keys may produce printable character via
+ // [aNativeKeyEvent characters]. Otherwise, translate input character of
+ // the key without control key.
+ else if (aKeyEvent.IsControl()) {
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags] & ~NSEventModifierFlagControl;
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (IsDeadKey(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ } else {
+ aKeyEvent.mKeyValue = TranslateToChar(nativeKeyCode, carbonState, kbType);
+ if (!aKeyEvent.mKeyValue.IsEmpty() && IsControlChar(aKeyEvent.mKeyValue[0])) {
+ // Don't expose control character to the web.
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ }
+ // Otherwise, KeyboardEvent.key expose
+ // [aNativeKeyEvent characters] value. However, if IME is open and the
+ // keyboard layout isn't ASCII capable, exposing the non-ASCII character
+ // doesn't match with other platform's behavior. For the compatibility
+ // with other platform's Gecko, we need to set a translated character.
+ else if (IsOpenedIMEMode()) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]);
+ aKeyEvent.mKeyValue = TranslateToChar(nativeKeyCode, state, kbType);
+ } else {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aKeyEvent.mKeyValue);
+ // If the key value is empty, the event may be a dead key event.
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ if (aKeyEvent.mKeyValue.IsEmpty()) {
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (TranslateToChar(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ }
+ }
+ }
+
+ // Last resort. If .key value becomes empty string, we should use
+ // charactersIgnoringModifiers, if it's available.
+ if (aKeyEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ (aKeyEvent.mKeyValue.IsEmpty() || IsControlChar(aKeyEvent.mKeyValue[0]))) {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent charactersIgnoringModifiers],
+ aKeyEvent.mKeyValue);
+ // But don't expose it if it's a control character.
+ if (!aKeyEvent.mKeyValue.IsEmpty() && IsControlChar(aKeyEvent.mKeyValue[0])) {
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ } else {
+ // Compute the key for non-printable keys and some special printable keys.
+ aKeyEvent.mKeyNameIndex = ComputeGeckoKeyNameIndex(nativeKeyCode);
+ }
+
+ aKeyEvent.mCodeNameIndex = ComputeGeckoCodeNameIndex(nativeKeyCode, kbType);
+ MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+void TISInputSourceWrapper::WillDispatchKeyboardEvent(NSEvent* aNativeKeyEvent,
+ const nsAString* aInsertString,
+ uint32_t aIndexOfKeypress,
+ WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Nothing to do here if the native key event is neither NSEventTypeKeyDown nor
+ // NSEventTypeKeyUp because accessing [aNativeKeyEvent characters] causes throwing
+ // an exception.
+ if ([aNativeKeyEvent type] != NSEventTypeKeyDown && [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ return;
+ }
+
+ UInt32 kbType = GetKbdType();
+
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ nsAutoString chars;
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars);
+ NS_ConvertUTF16toUTF8 utf8Chars(chars);
+ char16_t uniChar = static_cast<char16_t>(aKeyEvent.mCharCode);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aNativeKeyEvent=%p, aInsertString=%p (\"%s\"), "
+ "aIndexOfKeypress=%u, [aNativeKeyEvent characters]=\"%s\", "
+ "aKeyEvent={ mMessage=%s, mCharCode=0x%X(%s) }, kbType=0x%X, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, aInsertString,
+ aInsertString ? GetCharacters(*aInsertString) : "", aIndexOfKeypress,
+ GetCharacters([aNativeKeyEvent characters]), GetGeckoKeyEventType(aKeyEvent),
+ aKeyEvent.mCharCode, uniChar ? NS_ConvertUTF16toUTF8(&uniChar, 1).get() : "",
+ static_cast<unsigned int>(kbType), TrueOrFalse(IsOpenedIMEMode())));
+ }
+
+ nsAutoString insertStringForCharCode;
+ ComputeInsertStringForCharCode(aNativeKeyEvent, aKeyEvent, aInsertString,
+ insertStringForCharCode);
+
+ // The mCharCode was set from mKeyValue. However, for example, when Ctrl key
+ // is pressed, its value should indicate an ASCII character for backward
+ // compatibility rather than inputting character without the modifiers.
+ // Therefore, we need to modify mCharCode value here.
+ uint32_t charCode = 0;
+ if (aIndexOfKeypress < insertStringForCharCode.Length()) {
+ charCode = insertStringForCharCode[aIndexOfKeypress];
+ }
+ aKeyEvent.SetCharCode(charCode);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.mKeyCode=0x%X, aKeyEvent.mCharCode=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode));
+
+ // If aInsertString is not nullptr (it means InsertText() is called)
+ // and it acutally inputs a character, we don't need to append alternative
+ // charCode values since such keyboard event shouldn't be handled as
+ // a shortcut key.
+ if (aInsertString && charCode) {
+ return;
+ }
+
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ bool isRomanKeyboardLayout = IsASCIICapable();
+
+ UInt32 key = [aNativeKeyEvent keyCode];
+
+ // Caps lock and num lock modifier state:
+ UInt32 lockState = 0;
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagCapsLock) {
+ lockState |= alphaLock;
+ }
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagNumericPad) {
+ lockState |= kEventKeyModifierNumLockMask;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "isRomanKeyboardLayout=%s, kbType=0x%X, key=0x%X",
+ this, TrueOrFalse(isRomanKeyboardLayout), static_cast<unsigned int>(kbType),
+ static_cast<unsigned int>(key)));
+
+ nsString str;
+
+ // normal chars
+ uint32_t unshiftedChar = TranslateToChar(key, lockState, kbType);
+ UInt32 shiftLockMod = shiftKey | lockState;
+ uint32_t shiftedChar = TranslateToChar(key, shiftLockMod, kbType);
+
+ // characters generated with Cmd key
+ // XXX we should remove CapsLock state, which changes characters from
+ // Latin to Cyrillic with Russian layout on 10.4 only when Cmd key
+ // is pressed.
+ UInt32 numState = (lockState & ~alphaLock); // only num lock state
+ uint32_t uncmdedChar = TranslateToChar(key, numState, kbType);
+ UInt32 shiftNumMod = numState | shiftKey;
+ uint32_t uncmdedShiftChar = TranslateToChar(key, shiftNumMod, kbType);
+ uint32_t uncmdedUSChar = USLayout.TranslateToChar(key, numState, kbType);
+ UInt32 cmdNumMod = cmdKey | numState;
+ uint32_t cmdedChar = TranslateToChar(key, cmdNumMod, kbType);
+ UInt32 cmdShiftNumMod = shiftKey | cmdNumMod;
+ uint32_t cmdedShiftChar = TranslateToChar(key, cmdShiftNumMod, kbType);
+
+ // Is the keyboard layout changed by Cmd key?
+ // E.g., Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ bool isCmdSwitchLayout = uncmdedChar != cmdedChar;
+ // Is the keyboard layout for Latin, but Cmd key switches the layout?
+ // I.e., Dvorak-QWERTY
+ bool isDvorakQWERTY = isCmdSwitchLayout && isRomanKeyboardLayout;
+
+ // If the current keyboard is not Dvorak-QWERTY or Cmd is not pressed,
+ // we should append unshiftedChar and shiftedChar for handling the
+ // normal characters. These are the characters that the user is most
+ // likely to associate with this key.
+ if ((unshiftedChar || shiftedChar) && (!aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(unshiftedChar, shiftedChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.isMeta=%s, isDvorakQWERTY=%s, "
+ "unshiftedChar=U+%X, shiftedChar=U+%X",
+ this, OnOrOff(aKeyEvent.IsMeta()), TrueOrFalse(isDvorakQWERTY), unshiftedChar, shiftedChar));
+
+ // Most keyboard layouts provide the same characters in the NSEvents
+ // with Command+Shift as with Command. However, with Command+Shift we
+ // want the character on the second level. e.g. With a US QWERTY
+ // layout, we want "?" when the "/","?" key is pressed with
+ // Command+Shift.
+
+ // On a German layout, the OS gives us '/' with Cmd+Shift+SS(eszett)
+ // even though Cmd+SS is 'SS' and Shift+'SS' is '?'. This '/' seems
+ // like a hack to make the Cmd+"?" event look the same as the Cmd+"?"
+ // event on a US keyboard. The user thinks they are typing Cmd+"?", so
+ // we'll prefer the "?" character, replacing mCharCode with shiftedChar
+ // when Shift is pressed. However, in case there is a layout where the
+ // character unique to Cmd+Shift is the character that the user expects,
+ // we'll send it as an alternative char.
+ bool hasCmdShiftOnlyChar = cmdedChar != cmdedShiftChar && uncmdedShiftChar != cmdedShiftChar;
+ uint32_t originalCmdedShiftChar = cmdedShiftChar;
+
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock, which was
+ // ignored above.
+ if (!isCmdSwitchLayout) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ if (unshiftedChar) {
+ cmdedChar = unshiftedChar;
+ }
+ if (shiftedChar) {
+ cmdedShiftChar = shiftedChar;
+ }
+ } else if (uncmdedUSChar == cmdedChar) {
+ // It looks like characters from a US layout are provided when Command
+ // is down.
+ uint32_t ch = USLayout.TranslateToChar(key, lockState, kbType);
+ if (ch) {
+ cmdedChar = ch;
+ }
+ ch = USLayout.TranslateToChar(key, shiftLockMod, kbType);
+ if (ch) {
+ cmdedShiftChar = ch;
+ }
+ }
+
+ // If the current keyboard layout is switched by the Cmd key,
+ // we should append cmdedChar and shiftedCmdChar that are
+ // Latin char for the key.
+ // If the keyboard layout is Dvorak-QWERTY, we should append them only when
+ // command key is pressed because when command key isn't pressed, uncmded
+ // chars have been appended already.
+ if ((cmdedChar || cmdedShiftChar) && isCmdSwitchLayout &&
+ (aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(cmdedChar, cmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, isCmdSwitchLayout=%s, isDvorakQWERTY=%s, "
+ "cmdedChar=U+%X, cmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), TrueOrFalse(isDvorakQWERTY),
+ TrueOrFalse(isDvorakQWERTY), cmdedChar, cmdedShiftChar));
+ // Special case for 'SS' key of German layout. See the comment of
+ // hasCmdShiftOnlyChar definition for the detail.
+ if (hasCmdShiftOnlyChar && originalCmdedShiftChar) {
+ AlternativeCharCode altCharCodes(0, originalCmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, originalCmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), originalCmdedShiftChar));
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+uint32_t TISInputSourceWrapper::ComputeGeckoKeyCode(UInt32 aNativeKeyCode, UInt32 aKbType,
+ bool aCmdIsPressed) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, "
+ "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, "
+ "IsASCIICapable()=%s",
+ this, static_cast<unsigned int>(aNativeKeyCode), static_cast<unsigned int>(aKbType),
+ TrueOrFalse(aCmdIsPressed), TrueOrFalse(IsOpenedIMEMode()), TrueOrFalse(IsASCIICapable())));
+
+ switch (aNativeKeyCode) {
+ case kVK_Space:
+ return NS_VK_SPACE;
+ case kVK_Escape:
+ return NS_VK_ESCAPE;
+
+ // modifiers
+ case kVK_RightCommand:
+ case kVK_Command:
+ return NS_VK_META;
+ case kVK_RightShift:
+ case kVK_Shift:
+ return NS_VK_SHIFT;
+ case kVK_CapsLock:
+ return NS_VK_CAPS_LOCK;
+ case kVK_RightControl:
+ case kVK_Control:
+ return NS_VK_CONTROL;
+ case kVK_RightOption:
+ case kVK_Option:
+ return NS_VK_ALT;
+
+ case kVK_ANSI_KeypadClear:
+ return NS_VK_CLEAR;
+
+ // function keys
+ case kVK_F1:
+ return NS_VK_F1;
+ case kVK_F2:
+ return NS_VK_F2;
+ case kVK_F3:
+ return NS_VK_F3;
+ case kVK_F4:
+ return NS_VK_F4;
+ case kVK_F5:
+ return NS_VK_F5;
+ case kVK_F6:
+ return NS_VK_F6;
+ case kVK_F7:
+ return NS_VK_F7;
+ case kVK_F8:
+ return NS_VK_F8;
+ case kVK_F9:
+ return NS_VK_F9;
+ case kVK_F10:
+ return NS_VK_F10;
+ case kVK_F11:
+ return NS_VK_F11;
+ case kVK_F12:
+ return NS_VK_F12;
+ // case kVK_F13: return NS_VK_F13; // clash with the 3 below
+ // case kVK_F14: return NS_VK_F14;
+ // case kVK_F15: return NS_VK_F15;
+ case kVK_F16:
+ return NS_VK_F16;
+ case kVK_F17:
+ return NS_VK_F17;
+ case kVK_F18:
+ return NS_VK_F18;
+ case kVK_F19:
+ return NS_VK_F19;
+
+ case kVK_PC_Pause:
+ return NS_VK_PAUSE;
+ case kVK_PC_ScrollLock:
+ return NS_VK_SCROLL_LOCK;
+ case kVK_PC_PrintScreen:
+ return NS_VK_PRINTSCREEN;
+
+ // keypad
+ case kVK_ANSI_Keypad0:
+ return NS_VK_NUMPAD0;
+ case kVK_ANSI_Keypad1:
+ return NS_VK_NUMPAD1;
+ case kVK_ANSI_Keypad2:
+ return NS_VK_NUMPAD2;
+ case kVK_ANSI_Keypad3:
+ return NS_VK_NUMPAD3;
+ case kVK_ANSI_Keypad4:
+ return NS_VK_NUMPAD4;
+ case kVK_ANSI_Keypad5:
+ return NS_VK_NUMPAD5;
+ case kVK_ANSI_Keypad6:
+ return NS_VK_NUMPAD6;
+ case kVK_ANSI_Keypad7:
+ return NS_VK_NUMPAD7;
+ case kVK_ANSI_Keypad8:
+ return NS_VK_NUMPAD8;
+ case kVK_ANSI_Keypad9:
+ return NS_VK_NUMPAD9;
+
+ case kVK_ANSI_KeypadMultiply:
+ return NS_VK_MULTIPLY;
+ case kVK_ANSI_KeypadPlus:
+ return NS_VK_ADD;
+ case kVK_ANSI_KeypadMinus:
+ return NS_VK_SUBTRACT;
+ case kVK_ANSI_KeypadDecimal:
+ return NS_VK_DECIMAL;
+ case kVK_ANSI_KeypadDivide:
+ return NS_VK_DIVIDE;
+
+ case kVK_JIS_KeypadComma:
+ return NS_VK_SEPARATOR;
+
+ // IME keys
+ case kVK_JIS_Eisu:
+ return NS_VK_EISU;
+ case kVK_JIS_Kana:
+ return NS_VK_KANA;
+
+ // these may clash with forward delete and help
+ case kVK_PC_Insert:
+ return NS_VK_INSERT;
+ case kVK_PC_Delete:
+ return NS_VK_DELETE;
+
+ case kVK_PC_Backspace:
+ return NS_VK_BACK;
+ case kVK_Tab:
+ return NS_VK_TAB;
+
+ case kVK_Home:
+ return NS_VK_HOME;
+ case kVK_End:
+ return NS_VK_END;
+
+ case kVK_PageUp:
+ return NS_VK_PAGE_UP;
+ case kVK_PageDown:
+ return NS_VK_PAGE_DOWN;
+
+ case kVK_LeftArrow:
+ return NS_VK_LEFT;
+ case kVK_RightArrow:
+ return NS_VK_RIGHT;
+ case kVK_UpArrow:
+ return NS_VK_UP;
+ case kVK_DownArrow:
+ return NS_VK_DOWN;
+
+ case kVK_PC_ContextMenu:
+ return NS_VK_CONTEXT_MENU;
+
+ case kVK_ANSI_1:
+ return NS_VK_1;
+ case kVK_ANSI_2:
+ return NS_VK_2;
+ case kVK_ANSI_3:
+ return NS_VK_3;
+ case kVK_ANSI_4:
+ return NS_VK_4;
+ case kVK_ANSI_5:
+ return NS_VK_5;
+ case kVK_ANSI_6:
+ return NS_VK_6;
+ case kVK_ANSI_7:
+ return NS_VK_7;
+ case kVK_ANSI_8:
+ return NS_VK_8;
+ case kVK_ANSI_9:
+ return NS_VK_9;
+ case kVK_ANSI_0:
+ return NS_VK_0;
+
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Return:
+ case kVK_Powerbook_KeypadEnter:
+ return NS_VK_RETURN;
+ }
+
+ // If Cmd key is pressed, that causes switching keyboard layout temporarily.
+ // E.g., Dvorak-QWERTY. Therefore, if Cmd key is pressed, we should honor it.
+ UInt32 modifiers = aCmdIsPressed ? cmdKey : 0;
+
+ uint32_t charCode = TranslateToChar(aNativeKeyCode, modifiers, aKbType);
+
+ // Special case for Mac. Mac inputs Yen sign (U+00A5) directly instead of
+ // Back slash (U+005C). We should return NS_VK_BACK_SLASH for compatibility
+ // with other platforms.
+ // XXX How about Won sign (U+20A9) which has same problem as Yen sign?
+ if (charCode == 0x00A5) {
+ return NS_VK_BACK_SLASH;
+ }
+
+ uint32_t keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ // If the unshifed char isn't an ASCII character, use shifted char.
+ charCode = TranslateToChar(aNativeKeyCode, modifiers | shiftKey, aKbType);
+ keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ if (!IsASCIICapable()) {
+ // Retry with ASCII capable keyboard layout.
+ TISInputSourceWrapper currentKeyboardLayout;
+ currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout();
+ NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0);
+ keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType, aCmdIsPressed);
+ // We've returned 0 for long time if keyCode isn't for an alphabet keys or
+ // a numeric key even in alternative ASCII capable keyboard layout because
+ // we decided that we should avoid setting same keyCode value to 2 or
+ // more keys since active keyboard layout may have a key to input the
+ // punctuation with different key. However, setting keyCode to 0 makes
+ // some web applications which are aware of neither KeyboardEvent.key nor
+ // KeyboardEvent.code not work with Firefox when user selects non-ASCII
+ // capable keyboard layout such as Russian and Thai. So, if alternative
+ // ASCII capable keyboard layout has keyCode value for the key, we should
+ // use it. In other words, this behavior does that non-ASCII capable
+ // keyboard layout overrides some keys' keyCode value only if the key
+ // produces ASCII character by itself or with Shift key.
+ if (keyCode) {
+ return keyCode;
+ }
+ }
+
+ // Otherwise, let's decide keyCode value from the native virtual keycode
+ // value on major keyboard layout.
+ CodeNameIndex code = ComputeGeckoCodeNameIndex(aNativeKeyCode, aKbType);
+ return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code);
+}
+
+// static
+KeyNameIndex TISInputSourceWrapper::ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode) {
+ // NOTE:
+ // When unsupported keys like Convert, Nonconvert of Japanese keyboard is
+ // pressed:
+ // on 10.6.x, 'A' key event is fired (and also actually 'a' is inserted).
+ // on 10.7.x, Nothing happens.
+ // on 10.8.x, Nothing happens.
+ // on 10.9.x, FlagsChanged event is fired with keyCode 0xFF.
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ return KEY_NAME_INDEX_Unidentified;
+ }
+}
+
+// static
+CodeNameIndex TISInputSourceWrapper::ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode,
+ UInt32 aKbType) {
+ // macOS swaps native key code between Backquote key and IntlBackslash key
+ // only when the keyboard type is ISO. Let's treat the key code after
+ // swapping them here because Chromium does so only when computing .code
+ // value.
+ if (::KBGetLayoutType(aKbType) == kKeyboardISO) {
+ if (aNativeKeyCode == kVK_ISO_Section) {
+ aNativeKeyCode = kVK_ANSI_Grave;
+ } else if (aNativeKeyCode == kVK_ANSI_Grave) {
+ aNativeKeyCode = kVK_ISO_Section;
+ }
+ }
+
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+NSUInteger TextInputHandler::sLastModifierState = 0;
+
+// static
+CFArrayRef TextInputHandler::CreateAllKeyboardLayoutList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardLayout};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void TextInputHandler::DebugPrintAllKeyboardLayouts() {
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllKeyboardLayoutList();
+ MOZ_LOG(gKeyLog, LogLevel::Info, ("Keyboard layout configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource =
+ static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n", NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsKeyboardLayout() && tis.GetUCKeyboardLayout() ? "" : "\t(uchr is NOT AVAILABLE)"));
+ }
+ ::CFRelease(list);
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation
+ *
+ ******************************************************************************/
+
+TextInputHandler::TextInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : IMEInputHandler(aWidget, aNativeView) {
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+ [mView installTextInputHandler:this];
+}
+
+TextInputHandler::~TextInputHandler() { [mView uninstallTextInputHandler]; }
+
+bool TextInputHandler::HandleKeyDownEvent(NSEvent* aNativeEvent, uint32_t aUniqueId) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleKeyDownEvent, "
+ "widget has been already destroyed",
+ this));
+ return false;
+ }
+
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\"",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode],
+ [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers])));
+
+ // Except when Command key is pressed, we should hide mouse cursor until
+ // next mousemove. Handling here means that:
+ // - Don't hide mouse cursor at pressing modifier key
+ // - Hide mouse cursor even if the key event will be handled by IME (i.e.,
+ // even without dispatching eKeyPress events)
+ // - Hide mouse cursor even when a plugin has focus
+ if (!([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ [NSCursor setHiddenUntilMouseMoves:YES];
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent, aUniqueId);
+ AutoKeyEventStateCleaner remover(this);
+
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+
+ // When we're already in a composition, we need always to mark the eKeyDown
+ // event as "processed by IME". So, let's dispatch eKeyDown event here in
+ // such case.
+ if (IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // Let Cocoa interpret the key events, caching IsIMEComposing first.
+ bool wasComposing = IsIMEComposing();
+ bool interpretKeyEventsCalled = false;
+ // Don't call interpretKeyEvents when a plugin has focus. If we call it,
+ // for example, a character is inputted twice during a composition in e10s
+ // mode.
+ if (IsIMEEnabled() || IsASCIICapableOnly()) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, calling interpretKeyEvents", this));
+ [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]];
+ interpretKeyEventsCalled = true;
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents", this));
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed", this));
+ return currentKeyEvent->IsDefaultPrevented();
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, wasComposing=%s, "
+ "IsIMEComposing()=%s",
+ this, TrueOrFalse(wasComposing), TrueOrFalse(IsIMEComposing())));
+
+ if (currentKeyEvent->CanDispatchKeyDownEvent()) {
+ // Dispatch eKeyDown event if nobody has dispatched it yet.
+ // NOTE: Although reaching here means that the native keydown event may
+ // not be handled by IME. However, we cannot know if it is.
+ // For example, Japanese IME of Apple shows candidate window for
+ // typing window. They, you can switch the sort order with Tab key.
+ // However, when you choose "Symbol" of the sort order, there may
+ // be no candiate words. In this case, IME handles the Tab key
+ // actually, but we cannot know it because composition string is
+ // not updated. So, let's mark eKeyDown event as "processed by IME"
+ // when there is composition string. This is same as Chrome.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to dispatch eKeyDown "
+ "event since it's not yet dispatched",
+ this));
+ if (!MaybeDispatchCurrentKeydownEvent(IsIMEComposing())) {
+ return true; // treat the eKeydDown event as consumed.
+ }
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown event has been "
+ "dispatched",
+ this));
+ }
+
+ if (currentKeyEvent->CanDispatchKeyPressEvent() && !wasComposing && !IsIMEComposing()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::HandleKeyDownEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return false;
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ // If we called interpretKeyEvents and this isn't normal character input
+ // then IME probably ate the event for some reason. We do not want to
+ // send a key press event in that case.
+ // TODO:
+ // There are some other cases which IME eats the current event.
+ // 1. If key events were nested during calling interpretKeyEvents, it means
+ // that IME did something. Then, we should do nothing.
+ // 2. If one or more commands are called like "deleteBackward", we should
+ // dispatch keypress event at that time. Note that the command may have
+ // been a converted or generated action by IME. Then, we shouldn't do
+ // our default action for this key.
+ if (!(interpretKeyEventsCalled && IsNormalCharInputtingEvent(aNativeEvent))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to dispatch "
+ "eKeyPress event since it's not yet dispatched",
+ this));
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+ currentKeyEvent->mKeyPressDispatched = true;
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyPress event has been "
+ "dispatched",
+ this));
+ }
+ }
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, "
+ "keydown handled=%s, keypress handled=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, TrueOrFalse(currentKeyEvent->mKeyDownHandled),
+ TrueOrFalse(currentKeyEvent->mKeyPressHandled),
+ TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents),
+ TrueOrFalse(currentKeyEvent->mCompositionDispatched)));
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ return currentKeyEvent->IsDefaultPrevented();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\", "
+ "IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode],
+ [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers]), TrueOrFalse(IsIMEComposing())));
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleKeyUpEvent, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::HandleKeyUpEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ // Neither Chrome for macOS nor Safari marks "keyup" event as "processed by
+ // IME" even during composition. So, let's follow this behavior.
+ WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
+ InitKeyEvent(aNativeEvent, keyupEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status, &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleFlagsChanged, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ RefPtr<nsChildView> kungFuDeathGrip(mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not referenced within this function
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08lX, "
+ "sLastModifierState=0x%08lX, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode],
+ static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ static_cast<unsigned long>(sLastModifierState), TrueOrFalse(IsIMEComposing())));
+
+ MOZ_ASSERT([aNativeEvent type] == NSEventTypeFlagsChanged);
+
+ NSUInteger diff = [aNativeEvent modifierFlags] ^ sLastModifierState;
+ // Device dependent flags for left-control key, both shift keys, both command
+ // keys and both option keys have been defined in Next's SDK. But we
+ // shouldn't use it directly as far as possible since Cocoa SDK doesn't
+ // define them. Fortunately, we need them only when we dispatch keyup
+ // events. So, we can usually know the actual relation between keyCode and
+ // device dependent flags. However, we need to remove following flags first
+ // since the differences don't indicate modifier key state.
+ // NX_STYLUSPROXIMITYMASK: Probably used for pen like device.
+ // kCGEventFlagMaskNonCoalesced (= NX_NONCOALSESCEDMASK): See the document for
+ // Quartz Event Services.
+ diff &= ~(NX_STYLUSPROXIMITYMASK | kCGEventFlagMaskNonCoalesced);
+
+ switch ([aNativeEvent keyCode]) {
+ // CapsLock state and other modifier states are different:
+ // CapsLock state does not revert when the CapsLock key goes up, as the
+ // modifier state does for other modifier keys on key up.
+ case kVK_CapsLock: {
+ // Fire key down event for caps lock.
+ DispatchKeyEventForFlagsChanged(aNativeEvent, true);
+ // XXX should we fire keyup event too? The keyup event for CapsLock key
+ // is never dispatched on Gecko.
+ // XXX WebKit dispatches keydown event when CapsLock is locked, otherwise,
+ // keyup event. If we do so, we cannot keep the consistency with other
+ // platform's behavior...
+ break;
+ }
+
+ // If the event is caused by pressing or releasing a modifier key, just
+ // dispatch the key's event.
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_Help: {
+ // We assume that at most one modifier is changed per event if the event
+ // is caused by pressing or releasing a modifier key.
+ bool isKeyDown = ([aNativeEvent modifierFlags] & diff) != 0;
+ DispatchKeyEventForFlagsChanged(aNativeEvent, isKeyDown);
+ // XXX Some applications might send the event with incorrect device-
+ // dependent flags.
+ if (isKeyDown && ((diff & ~NSEventModifierFlagDeviceIndependentFlagsMask) != 0)) {
+ unsigned short keyCode = [aNativeEvent keyCode];
+ const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(diff);
+ if (modifierKey && modifierKey->keyCode != keyCode) {
+ // Although, we're not sure the actual cause of this case, the stored
+ // modifier information and the latest key event information may be
+ // mismatched. Then, let's reset the stored information.
+ // NOTE: If this happens, it may fail to handle NSEventTypeFlagsChanged event
+ // in the default case (below). However, it's the rare case handler
+ // and this case occurs rarely. So, we can ignore the edge case bug.
+ NS_WARNING("Resetting stored modifier key information");
+ mModifierKeys.Clear();
+ modifierKey = nullptr;
+ }
+ if (!modifierKey) {
+ mModifierKeys.AppendElement(ModifierKey(diff, keyCode));
+ }
+ }
+ break;
+ }
+
+ // Currently we don't support Fn key since other browsers don't dispatch
+ // events for it and we don't have keyCode for this key.
+ // It should be supported when we implement .key and .char.
+ case kVK_Function:
+ break;
+
+ // If the event is caused by something else than pressing or releasing a
+ // single modifier key (for example by the app having been deactivated
+ // using command-tab), use the modifiers themselves to determine which
+ // key's event to dispatch, and whether it's a keyup or keydown event.
+ // In all cases we assume one or more modifiers are being deactivated
+ // (never activated) -- otherwise we'd have received one or more events
+ // corresponding to a single modifier key being pressed.
+ default: {
+ NSUInteger modifiers = sLastModifierState;
+ AutoTArray<unsigned short, 10> dispatchedKeyCodes;
+ for (int32_t bit = 0; bit < 32; ++bit) {
+ NSUInteger flag = 1 << bit;
+ if (!(diff & flag)) {
+ continue;
+ }
+
+ // Given correct information from the application, a flag change here
+ // will normally be a deactivation (except for some lockable modifiers
+ // such as CapsLock). But some applications (like VNC) can send an
+ // activating event with a zero keyCode. So we need to check for that
+ // here.
+ bool dispatchKeyDown = ((flag & [aNativeEvent modifierFlags]) != 0);
+
+ unsigned short keyCode = 0;
+ if (flag & NSEventModifierFlagDeviceIndependentFlagsMask) {
+ switch (flag) {
+ case NSEventModifierFlagCapsLock:
+ keyCode = kVK_CapsLock;
+ dispatchKeyDown = true;
+ break;
+
+ case NSEventModifierFlagNumericPad:
+ // NSEventModifierFlagNumericPad is fired by VNC a lot. But not all of
+ // these events can really be Clear key events, so we just ignore
+ // them.
+ continue;
+
+ case NSEventModifierFlagHelp:
+ keyCode = kVK_Help;
+ break;
+
+ case NSEventModifierFlagFunction:
+ // An NSEventModifierFlagFunction change here will normally be a
+ // deactivation. But sometimes it will be an activation send (by
+ // VNC for example) with a zero keyCode.
+ continue;
+
+ // These cases (NSEventModifierFlagShift, NSEventModifierFlagControl,
+ // NSEventModifierFlagOption and NSEventModifierFlagCommand) should be handled by the
+ // other branch of the if statement, below (which handles device dependent flags).
+ // However, some applications (like VNC) can send key events without
+ // any device dependent flags, so we handle them here instead.
+ case NSEventModifierFlagShift:
+ keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift;
+ break;
+ case NSEventModifierFlagControl:
+ keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control;
+ break;
+ case NSEventModifierFlagOption:
+ keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option;
+ break;
+ case NSEventModifierFlagCommand:
+ keyCode = (modifiers & 0x0010) ? kVK_RightCommand : kVK_Command;
+ break;
+
+ default:
+ continue;
+ }
+ } else {
+ const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(flag);
+ if (!modifierKey) {
+ // See the note above (in the other branch of the if statement)
+ // about the NSEventModifierFlagShift, NSEventModifierFlagControl,
+ // NSEventModifierFlagOption and NSEventModifierFlagCommand cases.
+ continue;
+ }
+ keyCode = modifierKey->keyCode;
+ }
+
+ // Remove flags
+ modifiers &= ~flag;
+ switch (keyCode) {
+ case kVK_Shift: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightShift);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_RightShift: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Shift);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_Command: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightCommand);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_RightCommand: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Command);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_Control: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightControl);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_RightControl: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Control);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_Option: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightOption);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_RightOption: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Option);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_Help:
+ modifiers &= ~NSEventModifierFlagHelp;
+ break;
+ default:
+ break;
+ }
+
+ // Avoid dispatching same keydown/keyup events twice or more.
+ // We must be able to assume that there is no case to dispatch
+ // both keydown and keyup events with same key code value here.
+ if (dispatchedKeyCodes.Contains(keyCode)) {
+ continue;
+ }
+ dispatchedKeyCodes.AppendElement(keyCode);
+
+ NSEvent* event = [NSEvent keyEventWithType:NSEventTypeFlagsChanged
+ location:[aNativeEvent locationInWindow]
+ modifierFlags:modifiers
+ timestamp:[aNativeEvent timestamp]
+ windowNumber:[aNativeEvent windowNumber]
+ context:nil
+ characters:@""
+ charactersIgnoringModifiers:@""
+ isARepeat:NO
+ keyCode:keyCode];
+ DispatchKeyEventForFlagsChanged(event, dispatchKeyDown);
+ if (Destroyed()) {
+ break;
+ }
+
+ // Stop if focus has changed.
+ // Check to see if mView is still the first responder.
+ if (![mView isFirstResponder]) {
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ // Be aware, the widget may have been destroyed.
+ sLastModifierState = [aNativeEvent modifierFlags];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+const TextInputHandler::ModifierKey* TextInputHandler::GetModifierKeyForNativeKeyCode(
+ unsigned short aKeyCode) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].keyCode == aKeyCode) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+const TextInputHandler::ModifierKey* TextInputHandler::GetModifierKeyForDeviceDependentFlags(
+ NSUInteger aFlags) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].GetDeviceDependentFlags() ==
+ (aFlags & ~NSEventModifierFlagDeviceIndependentFlagsMask)) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+void TextInputHandler::DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent,
+ bool aDispatchKeyDown) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), aDispatchKeyDown=%s, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode],
+ TrueOrFalse(aDispatchKeyDown), TrueOrFalse(IsIMEComposing())));
+
+ if ([aNativeEvent type] != NSEventTypeFlagsChanged) {
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ EventMessage message = aDispatchKeyDown ? eKeyDown : eKeyUp;
+
+ // Fire a key event for the modifier key. Note that even if modifier key
+ // is pressed during composition, we shouldn't mark the keyboard event as
+ // "processed by IME" since neither Chrome for macOS nor Safari does it.
+ WidgetKeyboardEvent keyEvent(true, message, mWidget);
+ InitKeyEvent(aNativeEvent, keyEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(message, keyEvent, status, &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::InsertText(NSAttributedString* aAttrString, NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::InsertText, aAttrString=\"%s\", "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "IsIMEComposing()=%s, "
+ "keyevent=%p, keydownDispatched=%s, "
+ "keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, GetCharacters([aAttrString string]), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(IsIMEComposing()), currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+ InputContext context = mWidget->GetInputContext();
+ bool isEditable = (context.mIMEState.mEnabled == IMEEnabled::Enabled ||
+ context.mIMEState.mEnabled == IMEEnabled::Password);
+ NSRange selectedRange = SelectedRange();
+
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ AutoInsertStringClearer clearer(currentKeyEvent);
+ if (currentKeyEvent) {
+ currentKeyEvent->mInsertString = &str;
+ }
+
+ if (!IsIMEComposing() && str.IsEmpty()) {
+ // nothing to do if there is no content which can be removed.
+ if (!isEditable) {
+ return;
+ }
+ // If replacement range is specified, we need to remove the range.
+ // Otherwise, we need to remove the selected range if it's not collapsed.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound) {
+ // nothing to do since the range is collapsed.
+ if (aReplacementRange->length == 0) {
+ return;
+ }
+ // If the replacement range is different from current selected range,
+ // select the range.
+ if (!NSEqualRanges(selectedRange, *aReplacementRange)) {
+ NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+ }
+ selectedRange = SelectedRange();
+ }
+ NS_ENSURE_TRUE_VOID(selectedRange.location != NSNotFound);
+ if (selectedRange.length == 0) {
+ return; // nothing to do
+ }
+ // If this is caused by a key input, the keypress event which will be
+ // dispatched later should cause the delete. Therefore, nothing to do here.
+ // Although, we're not sure if such case is actually possible.
+ if (!currentKeyEvent) {
+ return;
+ }
+
+ // When current keydown event causes this empty text input, let's
+ // dispatch eKeyDown event before any other events. Note that if we're
+ // in a composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark this eKeyDown event as "processed by IME"?
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // Delete the selected range.
+ WidgetContentCommandEvent deleteCommandEvent(true, eContentCommandDelete, mWidget);
+ DispatchEvent(deleteCommandEvent);
+ NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded);
+ // Be aware! The widget might be destroyed here.
+ return;
+ }
+
+ bool isReplacingSpecifiedRange = isEditable && aReplacementRange &&
+ aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(selectedRange, *aReplacementRange);
+
+ // If this is not caused by pressing a key, there is a composition or
+ // replacing a range which is different from current selection, let's
+ // insert the text as committing a composition.
+ // If InsertText() is called two or more times, we should insert all
+ // text with composition events.
+ // XXX When InsertText() is called multiple times, Chromium dispatches
+ // only one composition event. So, we need to store InsertText()
+ // calls and flush later.
+ if (!currentKeyEvent || currentKeyEvent->mCompositionDispatched || IsIMEComposing() ||
+ isReplacingSpecifiedRange) {
+ InsertTextAsCommittingComposition(aAttrString, aReplacementRange);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ return;
+ }
+
+ // Don't let the same event be fired twice when hitting
+ // enter/return for Bug 420502. However, Korean IME (or some other
+ // simple IME) may work without marked text. For example, composing
+ // character may be inserted as committed text and it's modified with
+ // aReplacementRange. When a keydown starts new composition with
+ // committing previous character, InsertText() may be called twice,
+ // one is for committing previous character and then, inserting new
+ // composing character as committed character. In the latter case,
+ // |CanDispatchKeyPressEvent()| returns true but we need to dispatch
+ // keypress event for the new character. So, when IME tries to insert
+ // printable characters, we should ignore current key event state even
+ // after the keydown has already caused dispatching composition event.
+ // XXX Anyway, we should sort out around this at fixing bug 1338460.
+ if (currentKeyEvent && !currentKeyEvent->CanDispatchKeyPressEvent() &&
+ (str.IsEmpty() || (str.Length() == 1 && !IsPrintableChar(str[0])))) {
+ return;
+ }
+
+ // This is the normal path to input a character when you press a key.
+ // Let's dispatch eKeyDown event now.
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // XXX Shouldn't we hold mDispatcher instead of mWidget?
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::InsertText, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, "
+ "maybe dispatches eKeyPress event without control, alt and meta modifiers",
+ this));
+
+ // Dispatch keypress event with char instead of compositionchange event
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ // XXX Why do we need to dispatch keypress event for not inputting any
+ // string? If it wants to delete the specified range, should we
+ // dispatch an eContentCommandDelete event instead? Because this
+ // must not be caused by a key operation, a part of IME's processing.
+
+ // Don't set other modifiers from the current event, because here in
+ // -insertText: they've already been taken into account in creating
+ // the input string.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ nsCocoaUtils::InitInputEvent(keypressEvent, static_cast<NSEvent*>(nullptr));
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ keypressEvent.mKeyValue = str;
+ // FYI: TextEventDispatcher will set mKeyCode to 0 for printable key's
+ // keypress events even if they don't cause inputting non-empty string.
+ }
+
+ // TODO:
+ // If mCurrentKeyEvent.mKeyEvent is null, the text should be inputted as
+ // composition events.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool keyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyPressHandled = keyPressHandled;
+ currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool TextInputHandler::HandleCommand(Command aCommand) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleCommand, "
+ "aCommand=%s, IsIMEComposing()=%s, "
+ "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, ToChar(aCommand), TrueOrFalse(IsIMEComposing()),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+ // The command shouldn't be handled, let's ignore it.
+ if (currentKeyEvent && !currentKeyEvent->CanHandleCommand()) {
+ return false;
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // If it's in composition, we cannot dispatch keypress event.
+ // Therefore, we should use different approach or give up to handle
+ // the command.
+ if (IsIMEComposing()) {
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // Insert '\n' as committing composition.
+ // Otherwise, we need to dispatch keypress event because HTMLEditor
+ // doesn't treat "\n" in composition string as a line break unless
+ // the whitespace is treated as pre (see bug 1350541). In strictly
+ // speaking, we should dispatch keypress event as-is if it's handling
+ // NSEventTypeKeyDown event or should insert it with committing composition.
+ NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"];
+ InsertTextAsCommittingComposition(lineBreaker, nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ [lineBreaker release];
+ return true;
+ }
+ case Command::DeleteCharBackward:
+ case Command::DeleteCharForward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward:
+ case Command::DeleteWordForward:
+ // Don't remove any contents during composition.
+ return false;
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ // Don't move focus during composition.
+ return false;
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine:
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine:
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom:
+ case Command::SelectPageUp:
+ case Command::SelectPageDown:
+ case Command::ScrollBottom:
+ case Command::ScrollTop:
+ // Don't move selection during composition.
+ return false;
+ case Command::CancelOperation:
+ case Command::Complete:
+ // Don't handle Escape key by ourselves during composition.
+ return false;
+ case Command::ScrollPageUp:
+ case Command::ScrollPageDown:
+ // Allow to scroll.
+ break;
+ default:
+ break;
+ }
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p, TextInputHandler::HandleCommand, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // TODO: If it's not appropriate keypress but user customized the OS
+ // settings to do the command with other key, we should just set
+ // command to the keypress event and it should be handled as
+ // the key press in editor.
+
+ // If it's handling actual key event and hasn't cause any composition
+ // events nor other key events, we should expose actual modifier state.
+ // Otherwise, we should adjust Control, Option and Command state since
+ // editor may behave differently if some of them are active.
+ bool dispatchFakeKeyPress = !(currentKeyEvent && currentKeyEvent->IsProperKeyEvent(aCommand));
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ if (!dispatchFakeKeyPress) {
+ // If we're acutally handling a key press, we should dispatch
+ // the keypress event as-is.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent, false);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ // Otherwise, we should dispatch "fake" keypress event.
+ // However, for making it possible to compute edit commands, we need to
+ // set current native key event to the fake keyboard event even if it's
+ // not same as what we expect since the native keyboard event caused
+ // this command.
+ NSEvent* keyEvent = currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr;
+ keydownEvent.mNativeKeyEvent = keypressEvent.mNativeKeyEvent = keyEvent;
+ NS_WARNING_ASSERTION(keypressEvent.mNativeKeyEvent,
+ "Without native key event, NativeKeyBindings cannot compute aCommand");
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // Although, Shift+Enter and Enter are work differently in HTML
+ // editor, we should expose actual Shift state if it's caused by
+ // Enter key for compatibility with Chromium. Chromium breaks
+ // line in HTML editor with default pargraph separator when Enter
+ // is pressed, with <br> element when Shift+Enter. Safari breaks
+ // line in HTML editor with default paragraph separator when
+ // Enter, Shift+Enter or Option+Enter. So, we should not change
+ // Shift+Enter meaning when there was composition string or not.
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RETURN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Enter;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertLineBreak) {
+ // In default settings, Ctrl + Enter causes insertLineBreak command.
+ // So, let's make Ctrl state active of the keypress event.
+ keypressEvent.mModifiers |= MODIFIER_CONTROL;
+ }
+ break;
+ }
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_TAB;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Tab;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertBacktab) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ case Command::DeleteCharBackward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_BACK;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Backspace;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteToBeginningOfLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ } else if (aCommand == Command::DeleteWordBackward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::DeleteCharForward:
+ case Command::DeleteWordForward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DELETE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Delete;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteWordForward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RIGHT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowRight;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharNext || aCommand == Command::SelectWordNext ||
+ aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordNext || aCommand == Command::SelectWordNext) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::EndLine || aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_LEFT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowLeft;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharPrevious || aCommand == Command::SelectWordPrevious ||
+ aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordPrevious || aCommand == Command::SelectWordPrevious) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::BeginLine || aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::SelectTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowUp;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLinePrevious || aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveTop || aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowDown;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLineNext || aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveBottom || aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::ScrollPageUp:
+ case Command::SelectPageUp: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageUp;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageUp) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollPageDown:
+ case Command::SelectPageDown: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageDown;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageDown) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollBottom:
+ case Command::ScrollTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ if (aCommand == Command::ScrollBottom) {
+ keypressEvent.mKeyCode = NS_VK_END;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_End;
+ } else {
+ keypressEvent.mKeyCode = NS_VK_HOME;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Home;
+ }
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ break;
+ }
+ case Command::CancelOperation:
+ case Command::Complete: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_ESCAPE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Escape;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::Complete) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+
+ nsCocoaUtils::InitInputEvent(keydownEvent, keyEvent);
+ keydownEvent.mKeyCode = keypressEvent.mKeyCode;
+ keydownEvent.mKeyNameIndex = keypressEvent.mKeyNameIndex;
+ keydownEvent.mModifiers = keypressEvent.mModifiers;
+ }
+
+ // We've stopped dispatching "keypress" events of non-printable keys on
+ // the web. Therefore, we need to dispatch eKeyDown event here for web
+ // apps. This is non-standard behavior if we've already dispatched a
+ // "keydown" event. However, Chrome also dispatches such fake "keydown"
+ // (and "keypress") event for making same behavior as Safari.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ if (mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, nullptr)) {
+ bool keydownHandled = status == nsEventStatus_eConsumeNoDefault;
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyDownDispatched = true;
+ currentKeyEvent->mKeyDownHandled |= keydownHandled;
+ }
+ if (keydownHandled) {
+ // Don't dispatch eKeyPress event if preceding eKeyDown event is
+ // consumed for conforming to UI Events.
+ // XXX Perhaps, we should ignore previous eKeyDown event result
+ // even if we've already dispatched because it may notify web apps
+ // of different key information, e.g., it's handled by IME, but
+ // web apps want to handle only this key.
+ return true;
+ }
+ }
+
+ bool keyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ // NOTE: mWidget might have become null here.
+
+ if (keyPressDispatched) {
+ // Record the keypress event state only when it dispatched actual Enter
+ // keypress event because in other cases, the keypress event just a
+ // messenger. E.g., if it's caused by different key, keypress event for
+ // the actual key should be dispatched.
+ if (!dispatchFakeKeyPress && currentKeyEvent) {
+ currentKeyEvent->mKeyPressHandled = keyPressHandled;
+ currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+ }
+ return true;
+ }
+
+ // If keypress event isn't dispatched as expected, we should fallback to
+ // using composition events.
+ if (aCommand == Command::InsertLineBreak || aCommand == Command::InsertParagraph) {
+ NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"];
+ InsertTextAsCommittingComposition(lineBreaker, nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ [lineBreaker release];
+ return true;
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool TextInputHandler::DoCommandBySelector(const char* aSelector) {
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", "
+ "Destroyed()=%s, keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, keypressHandled=%s, causedOtherKeyEvents=%s",
+ this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()),
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A"));
+
+ // If the command isn't caused by key operation, the command should
+ // be handled in the super class of the caller.
+ if (!currentKeyEvent) {
+ return Destroyed();
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return true;
+ }
+
+ // If the key operation causes this command, should dispatch a keypress
+ // event.
+ // XXX This must be worng. Even if this command is caused by the key
+ // operation, its our default action can be different from the
+ // command. So, in this case, we should dispatch a keypress event
+ // which have the command and editor should handle it.
+ if (currentKeyEvent->CanDispatchKeyPressEvent()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::DoCommandBySelector, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return Destroyed();
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, keypress event "
+ "dispatched, Destroyed()=%s, keypressHandled=%s",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(currentKeyEvent->mKeyPressHandled)));
+ // This command is now dispatched with keypress event.
+ // So, this shouldn't be handled by nobody anymore.
+ return true;
+ }
+
+ // If the key operation didn't cause keypress event or caused keypress event
+ // but not prevented its default, we need to honor the command. For example,
+ // Korean IME sends "insertNewline:" when committing existing composition
+ // with Enter key press. In such case, the key operation has been consumed
+ // by the committing composition but we still need to handle the command.
+ if (Destroyed() || !currentKeyEvent->CanHandleCommand()) {
+ return true;
+ }
+
+ // cancelOperation: command is fired after Escape or Command + Period.
+ // However, if ChildView implements cancelOperation:, calling
+ // [[ChildView super] doCommandBySelector:aSelector] when Command + Period
+ // causes only a call of [ChildView cancelOperation:sender]. I.e.,
+ // [ChildView keyDown:theEvent] becomes to be never called. For avoiding
+ // this odd behavior, we need to handle the command before super class of
+ // ChildView only when current key event is proper event to fire Escape
+ // keypress event.
+ if (!strcmp(aSelector, "cancelOperation:") && currentKeyEvent &&
+ currentKeyEvent->IsProperKeyEvent(Command::CancelOperation)) {
+ return HandleCommand(Command::CancelOperation);
+ }
+
+ // Otherwise, we've not handled the command yet. Propagate the command
+ // to the super class of ChildView.
+ return false;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+bool IMEInputHandler::sStaticMembersInitialized = false;
+bool IMEInputHandler::sCachedIsForRTLLangage = false;
+CFStringRef IMEInputHandler::sLatestIMEOpenedModeInputSourceID = nullptr;
+IMEInputHandler* IMEInputHandler::sFocusedIMEHandler = nullptr;
+
+// static
+void IMEInputHandler::InitStaticMembers() {
+ if (sStaticMembersInitialized) return;
+ sStaticMembersInitialized = true;
+ // We need to check the keyboard layout changes on all applications.
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ // XXX Don't we need to remove the observer at shut down?
+ // Mac Dev Center's document doesn't say how to remove the observer if
+ // the second parameter is NULL.
+ ::CFNotificationCenterAddObserver(center, NULL, OnCurrentTextInputSourceChange,
+ kTISNotifySelectedKeyboardInputSourceChanged, NULL,
+ CFNotificationSuspensionBehaviorDeliverImmediately);
+ // Initiailize with the current keyboard layout
+ OnCurrentTextInputSourceChange(NULL, NULL, kTISNotifySelectedKeyboardInputSourceChanged, NULL,
+ NULL);
+}
+
+// static
+void IMEInputHandler::OnCurrentTextInputSourceChange(CFNotificationCenterRef aCenter,
+ void* aObserver, CFStringRef aName,
+ const void* aObject,
+ CFDictionaryRef aUserInfo) {
+ // Cache the latest IME opened mode to sLatestIMEOpenedModeInputSourceID.
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ if (tis.IsOpenedIMEMode()) {
+ tis.GetInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ // Collect Input Source ID which includes input mode in most cases.
+ // However, if it's Japanese IME, collecting input mode (e.g.,
+ // "HiraganaKotei") does not make sense because in most languages,
+ // input mode changes "how to input", but Japanese IME changes
+ // "which type of characters to input". I.e., only Japanese IME
+ // users may use multiple input modes. If we'd collect each type of
+ // input mode of Japanese IMEs, it'd be difficult to count actual
+ // users of each IME from the result. So, only when active IME is
+ // a Japanese IME, we should use Bundle ID which does not contain
+ // input mode instead.
+ nsAutoString key;
+ if (tis.IsForJapaneseLanguage()) {
+ tis.GetBundleID(key);
+ } else {
+ tis.GetInputSourceID(key);
+ }
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ if (key.Length() > 72) {
+ if (NS_IS_SURROGATE_PAIR(key[72 - 2], key[72 - 1])) {
+ key.Truncate(72 - 2);
+ } else {
+ key.Truncate(72 - 1);
+ }
+ // U+2026 is "..."
+ key.Append(char16_t(0x2026));
+ }
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_MAC, key, true);
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ static CFStringRef sLastTIS = nullptr;
+ CFStringRef newTIS;
+ tis.GetInputSourceID(newTIS);
+ if (!sLastTIS || ::CFStringCompare(sLastTIS, newTIS, 0) != kCFCompareEqualTo) {
+ TISInputSourceWrapper tis1, tis2, tis3, tis4, tis5;
+ tis1.InitByCurrentKeyboardLayout();
+ tis2.InitByCurrentASCIICapableInputSource();
+ tis3.InitByCurrentASCIICapableKeyboardLayout();
+ tis4.InitByCurrentInputMethodKeyboardLayoutOverride();
+ tis5.InitByTISInputSourceRef(tis.GetKeyboardLayoutInputSource());
+ CFStringRef is0 = nullptr, is1 = nullptr, is2 = nullptr, is3 = nullptr, is4 = nullptr,
+ is5 = nullptr, type0 = nullptr, lang0 = nullptr, bundleID0 = nullptr;
+ tis.GetInputSourceID(is0);
+ tis1.GetInputSourceID(is1);
+ tis2.GetInputSourceID(is2);
+ tis3.GetInputSourceID(is3);
+ tis4.GetInputSourceID(is4);
+ tis5.GetInputSourceID(is5);
+ tis.GetInputSourceType(type0);
+ tis.GetPrimaryLanguage(lang0);
+ tis.GetBundleID(bundleID0);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMEInputHandler::OnCurrentTextInputSourceChange,\n"
+ " Current Input Source is changed to:\n"
+ " currentInputContext=%p\n"
+ " %s\n"
+ " type=%s %s\n"
+ " overridden keyboard layout=%s\n"
+ " used keyboard layout for translation=%s\n"
+ " primary language=%s\n"
+ " bundle ID=%s\n"
+ " current ASCII capable Input Source=%s\n"
+ " current Keyboard Layout=%s\n"
+ " current ASCII capable Keyboard Layout=%s",
+ [NSTextInputContext currentInputContext], GetCharacters(is0), GetCharacters(type0),
+ tis.IsASCIICapable() ? "- ASCII capable " : "", GetCharacters(is4),
+ GetCharacters(is5), GetCharacters(lang0), GetCharacters(bundleID0),
+ GetCharacters(is2), GetCharacters(is1), GetCharacters(is3)));
+ }
+ sLastTIS = newTIS;
+ }
+
+ /**
+ * When the direction is changed, all the children are notified.
+ * No need to treat the initial case separately because it is covered
+ * by the general case (sCachedIsForRTLLangage is initially false)
+ */
+ if (sCachedIsForRTLLangage != tis.IsForRTLLanguage()) {
+ WidgetUtils::SendBidiKeyboardInfoToContent();
+ sCachedIsForRTLLangage = tis.IsForRTLLanguage();
+ }
+}
+
+// static
+void IMEInputHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure) {
+ NS_ASSERTION(aClosure, "aClosure is null");
+ static_cast<IMEInputHandler*>(aClosure)->ExecutePendingMethods();
+}
+
+// static
+CFArrayRef IMEInputHandler::CreateAllIMEModeList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardInputMode};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void IMEInputHandler::DebugPrintAllIMEModes() {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllIMEModeList();
+ MOZ_LOG(gIMELog, LogLevel::Info, ("IME mode configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource =
+ static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid, bundleID;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ tis.GetBundleID(bundleID);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n"
+ " bundled in <%s>\n",
+ NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsEnabled() ? "" : "\t(Isn't Enabled)", NS_ConvertUTF16toUTF8(bundleID).get()));
+ }
+ ::CFRelease(list);
+ }
+}
+
+// static
+TSMDocumentID IMEInputHandler::GetCurrentTSMDocumentID() {
+ // At least on Mac OS X 10.6.x and 10.7.x, ::TSMGetActiveDocument() has a bug.
+ // The result of ::TSMGetActiveDocument() isn't modified for new active text
+ // input context until [NSTextInputContext currentInputContext] is called.
+ // Therefore, we need to call it here.
+ [NSTextInputContext currentInputContext];
+ return ::TSMGetActiveDocument();
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #1
+ * The methods are releated to the pending methods. Some jobs should be
+ * run after the stack is finished, e.g, some methods cannot run the jobs
+ * during processing the focus event. And also some other jobs should be
+ * run at the next focus event is processed.
+ * The pending methods are recorded in mPendingMethods. They are executed
+ * by ExecutePendingMethods via FlushPendingMethods.
+ *
+ ******************************************************************************/
+
+nsresult IMEInputHandler::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ CommitIMEComposition();
+ return NS_OK;
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ CancelIMEComposition();
+ return NS_OK;
+ case NOTIFY_IME_OF_FOCUS:
+ if (IsFocused()) {
+ nsIWidget* widget = aTextEventDispatcher->GetWidget();
+ if (widget && widget->GetInputContext().IsPasswordEditor()) {
+ EnableSecureEventInput();
+ } else {
+ EnsureSecureEventInputDisabled();
+ }
+ }
+ OnFocusChangeInGecko(true);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ OnFocusChangeInGecko(false);
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ OnSelectionChange(aNotification);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ OnLayoutChange();
+ return NS_OK;
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+IMEInputHandler::GetIMENotificationRequests() {
+ // XXX Shouldn't we move floating window which shows composition string
+ // when plugin has focus and its parent is scrolled or the window is
+ // moved?
+ return IMENotificationRequests();
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) {
+ // If the keyboard event is not caused by a native key event, we can do
+ // nothing here.
+ if (!aData) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = static_cast<KeyEventState*>(aData);
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ nsAString* insertString = currentKeyEvent->mInsertString;
+ if (aKeyboardEvent.mMessage == eKeyPress && aIndexOfKeypress == 0 &&
+ (!insertString || insertString->IsEmpty())) {
+ // Inform the child process that this is an event that we want a reply
+ // from.
+ // XXX This should be called only when the target is a remote process.
+ // However, it's difficult to check it under widget/.
+ // So, let's do this here for now, then,
+ // EventStateManager::PreHandleEvent() will reset the flags if
+ // the event target isn't in remote process.
+ aKeyboardEvent.MarkAsWaitingReplyFromRemoteProcess();
+ }
+ if (KeyboardLayoutOverrideRef().mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(KeyboardLayoutOverrideRef().mKeyboardLayout, true);
+ tis.WillDispatchKeyboardEvent(nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent);
+ } else {
+ TISInputSourceWrapper::CurrentInputSource().WillDispatchKeyboardEvent(
+ nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent);
+ }
+
+ // Remove basic modifiers from keypress event because if they are included
+ // but this causes inputting text, since TextEditor won't handle eKeyPress
+ // events whose ctrlKey, altKey or metaKey is true as text input.
+ // Note that this hack should be used only when an editor has focus because
+ // this is a hack for TextEditor and modifier key information may be
+ // important for current web app.
+ if (IsEditableContent() && insertString && aKeyboardEvent.mMessage == eKeyPress &&
+ aKeyboardEvent.mCharCode) {
+ aKeyboardEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ }
+}
+
+void IMEInputHandler::NotifyIMEOfFocusChangeInGecko() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::NotifyIMEOfFocusChangeInGecko, "
+ "Destroyed()=%s, IsFocused()=%s, inputContext=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ mView ? [mView inputContext] : nullptr));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ return;
+ }
+
+ MOZ_ASSERT(mView);
+ NSTextInputContext* inputContext = [mView inputContext];
+ NS_ENSURE_TRUE_VOID(inputContext);
+
+ // When an <input> element on a XUL <panel> element gets focus from an <input>
+ // element on the opener window of the <panel> element, the owner window
+ // still has native focus. Therefore, IMEs may store the opener window's
+ // level at this time because they don't know the actual focus is moved to
+ // different window. If IMEs try to get the newest window level after the
+ // focus change, we return the window level of the XUL <panel>'s widget.
+ // Therefore, let's emulate the native focus change. Then, IMEs can refresh
+ // the stored window level.
+ [inputContext deactivate];
+ [inputContext activate];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SyncASCIICapableOnly() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SyncASCIICapableOnly, "
+ "Destroyed()=%s, IsFocused()=%s, mIsASCIICapableOnly=%s, "
+ "GetCurrentTSMDocumentID()=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ TrueOrFalse(mIsASCIICapableOnly), GetCurrentTSMDocumentID()));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kSyncASCIICapableOnly;
+ return;
+ }
+
+ TSMDocumentID doc = GetCurrentTSMDocumentID();
+ if (!doc) {
+ // retry
+ mPendingMethods |= kSyncASCIICapableOnly;
+ NS_WARNING("Application is active but there is no active document");
+ ResetTimer();
+ return;
+ }
+
+ if (mIsASCIICapableOnly) {
+ CFArrayRef ASCIICapableTISList = ::TISCreateASCIICapableInputSourceList();
+ ::TSMSetDocumentProperty(doc, kTSMDocumentEnabledInputSourcesPropertyTag, sizeof(CFArrayRef),
+ &ASCIICapableTISList);
+ ::CFRelease(ASCIICapableTISList);
+ } else {
+ ::TSMRemoveDocumentProperty(doc, kTSMDocumentEnabledInputSourcesPropertyTag);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::ResetTimer() {
+ NS_ASSERTION(mPendingMethods != 0, "There are not pending methods, why this is called?");
+ if (mTimer) {
+ mTimer->Cancel();
+ } else {
+ mTimer = NS_NewTimer();
+ NS_ENSURE_TRUE(mTimer, );
+ }
+ mTimer->InitWithNamedFuncCallback(FlushPendingMethods, this, 0, nsITimer::TYPE_ONE_SHOT,
+ "IMEInputHandler::FlushPendingMethods");
+}
+
+void IMEInputHandler::ExecutePendingMethods() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ if (![[NSApplication sharedApplication] isActive]) {
+ // If we're not active, we should retry at focus event
+ return;
+ }
+
+ uint32_t pendingMethods = mPendingMethods;
+ // First, reset the pending method flags because if each methods cannot
+ // run now, they can reentry to the pending flags by theirselves.
+ mPendingMethods = 0;
+
+ if (pendingMethods & kSyncASCIICapableOnly) SyncASCIICapableOnly();
+ if (pendingMethods & kNotifyIMEOfFocusChangeInGecko) {
+ NotifyIMEOfFocusChangeInGecko();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (native event handlers)
+ *
+ ******************************************************************************/
+
+TextRangeType IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle,
+ NSRange& aSelectedRange) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::ConvertToTextRangeType, "
+ "aUnderlineStyle=%u, aSelectedRange.length=%lu,",
+ this, aUnderlineStyle, static_cast<unsigned long>(aSelectedRange.length)));
+
+ // We assume that aUnderlineStyle is NSUnderlineStyleSingle or
+ // NSUnderlineStyleThick. NSUnderlineStyleThick should indicate a selected
+ // clause. Otherwise, should indicate non-selected clause.
+
+ if (aSelectedRange.length == 0) {
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eRawClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedRawClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedRawClause;
+ }
+ }
+
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eConvertedClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedClause;
+ }
+}
+
+uint32_t IMEInputHandler::GetRangeCount(NSAttributedString* aAttrString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Iterate through aAttrString for the NSUnderlineStyleAttributeName and
+ // count the different segments adjusting limitRange as we go.
+ uint32_t count = 0;
+ NSRange effectiveRange;
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ while (limitRange.length > 0) {
+ [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+ limitRange = NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ count++;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%u", this,
+ GetCharacters([aAttrString string]), count));
+
+ return count;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0);
+}
+
+already_AddRefed<mozilla::TextRangeArray> IMEInputHandler::CreateTextRangeArray(
+ NSAttributedString* aAttrString, NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ RefPtr<mozilla::TextRangeArray> textRangeArray = new mozilla::TextRangeArray();
+
+ // Note that we shouldn't append ranges when composition string
+ // is empty because it may cause TextComposition confused.
+ if (![aAttrString length]) {
+ return textRangeArray.forget();
+ }
+
+ // Convert the Cocoa range into the TextRange Array used in Gecko.
+ // Iterate through the attributed string and map the underline attribute to
+ // Gecko IME textrange attributes. We may need to change the code here if
+ // we change the implementation of validAttributesForMarkedText.
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ uint32_t rangeCount = GetRangeCount(aAttrString);
+ for (uint32_t i = 0; i < rangeCount && limitRange.length > 0; i++) {
+ NSRange effectiveRange;
+ id attributeValue = [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+
+ TextRange range;
+ range.mStartOffset = effectiveRange.location;
+ range.mEndOffset = NSMaxRange(effectiveRange);
+ range.mRangeType = ConvertToTextRangeType([attributeValue intValue], aSelectedRange);
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ limitRange = NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ }
+
+ // Get current caret position.
+ TextRange range;
+ range.mStartOffset = aSelectedRange.location + aSelectedRange.length;
+ range.mEndOffset = range.mStartOffset;
+ range.mRangeType = TextRangeType::eCaret;
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ return textRangeArray.forget();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+bool IMEInputHandler::DispatchCompositionStartEvent() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "mSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, "
+ "mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, static_cast<unsigned long>(SelectedRange().location),
+ static_cast<unsigned long>(mSelectedRange.length), TrueOrFalse(Destroyed()), mView,
+ mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
+ mIsIMEComposing = true;
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ mIsDeadKeyComposing =
+ currentKeyEvent && currentKeyEvent->mKeyEvent &&
+ TISInputSourceWrapper::CurrentInputSource().IsDeadKey(currentKeyEvent->mKeyEvent);
+
+ nsEventStatus status;
+ rv = mDispatcher->StartComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to StartComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "destroyed by compositionstart event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ if (!mIsIMEComposing) {
+ return false;
+ }
+
+ // FYI: The selection range might have been modified by a compositionstart
+ // event handler.
+ mIMECompositionStart = SelectedRange().location;
+ return true;
+}
+
+bool IMEInputHandler::DispatchCompositionChangeEvent(const nsString& aText,
+ NSAttributedString* aAttrString,
+ NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "aText=\"%s\", aAttrString=\"%s\", "
+ "aSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, mView=%p, "
+ "mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, NS_ConvertUTF16toUTF8(aText).get(), GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length), TrueOrFalse(Destroyed()), mView,
+ mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ RefPtr<TextRangeArray> rangeArray = CreateTextRangeArray(aAttrString, aSelectedRange);
+
+ rv = mDispatcher->SetPendingComposition(aText, rangeArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to SetPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ mSelectedRange.location = mIMECompositionStart + aSelectedRange.location;
+ mSelectedRange.length = aSelectedRange.length;
+
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ }
+ mIMECompositionString = [[aAttrString string] retain];
+
+ nsEventStatus status;
+ rv = mDispatcher->FlushPendingComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to FlushPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "destroyed by compositionchange event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ return mIsIMEComposing;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::DispatchCompositionCommitEvent(const nsAString* aCommitString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "aCommitString=0x%p (\"%s\"), Destroyed()=%s, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, aCommitString, aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "",
+ TrueOrFalse(Destroyed()), mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing)));
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ if (!Destroyed()) {
+ // IME may query selection immediately after this, however, in e10s mode,
+ // OnSelectionChange() will be called asynchronously. Until then, we
+ // should emulate expected selection range if the webapp does nothing.
+ mSelectedRange.location = mIMECompositionStart;
+ if (aCommitString) {
+ mSelectedRange.location += aCommitString->Length();
+ } else if (mIMECompositionString) {
+ nsAutoString commitString;
+ nsCocoaUtils::GetStringForNSString(mIMECompositionString, commitString);
+ mSelectedRange.location += commitString.Length();
+ }
+ mSelectedRange.length = 0;
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ } else {
+ nsEventStatus status;
+ rv = mDispatcher->CommitComposition(status, aCommitString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ }
+ }
+ }
+
+ mIsIMEComposing = mIsDeadKeyComposing = false;
+ mIMECompositionStart = UINT32_MAX;
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "destroyed by compositioncommit event",
+ this));
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return false;
+ }
+ MOZ_ASSERT(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ if (!currentKeyEvent || !currentKeyEvent->CanDispatchKeyDownEvent()) {
+ return true;
+ }
+
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ if (NS_WARN_IF(!nativeEvent) || [nativeEvent type] != NSEventTypeKeyDown) {
+ return true;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, aIsProcessedByIME=%s "
+ "currentKeyEvent={ mKeyEvent(%p)={ type=%s, keyCode=%s (0x%X) } }, "
+ "aIsProcessedBy=%s, IsDeadKeyComposing()=%s",
+ this, TrueOrFalse(aIsProcessedByIME), nativeEvent, GetNativeKeyEventType(nativeEvent),
+ GetKeyNameForNativeKeyCode([nativeEvent keyCode]), [nativeEvent keyCode],
+ TrueOrFalse(IsIMEComposing()), TrueOrFalse(IsDeadKeyComposing())));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ RefPtr<TextEventDispatcher> dispatcher(mDispatcher);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NSResponder* firstResponder = [[mView window] firstResponder];
+
+ // Mark currentKeyEvent as "dispatched eKeyDown event" and actually do it.
+ currentKeyEvent->mKeyDownDispatched = true;
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ // Don't mark the eKeyDown event as "processed by IME" if the composition
+ // is started with dead key.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent, aIsProcessedByIME && !IsDeadKeyComposing());
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ dispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyDownHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "widget was destroyed by keydown event",
+ this));
+ return false;
+ }
+
+ // The key down event may have shifted the focus, in which case, we should
+ // not continue to handle current key sequence and let's commit current
+ // composition.
+ if (firstResponder != [[mView window] firstResponder]) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "view lost focus by keydown event",
+ this));
+ CommitIMEComposition();
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void IMEInputHandler::InsertTextAsCommittingComposition(NSAttributedString* aAttrString,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "aAttrString=\"%s\", aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, GetCharacters([aAttrString string]), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (IgnoreIMECommit()) {
+ MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not"
+ "be called while canceling the composition");
+ }
+
+ if (Destroyed()) {
+ return;
+ }
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark the eKeyDown event as "processed by IME"?
+ // However, if the key causes two or more Unicode characters as
+ // UTF-16 string, this is used. So, perhaps, we need to improve
+ // HandleKeyDownEvent() before do that.
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, eKeyDown "
+ "caused focus move or something and canceling the composition",
+ this));
+ return;
+ }
+
+ // First, commit current composition with the latest composition string if the
+ // replacement range is different from marked range.
+ if (IsIMEComposing() && aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(MarkedRange(), *aReplacementRange)) {
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ if (!IsIMEComposing()) {
+ MOZ_DIAGNOSTIC_ASSERT(!str.IsEmpty());
+
+ // If there is no selection and replacement range is specified, set the
+ // range as selection.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+ NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+ }
+
+ if (!StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
+ // In the default settings, we should not use composition events for
+ // inserting text without key press nor IME composition because the
+ // other browsers do so. This will cause only a cancelable `beforeinput`
+ // event whose `inputType` is `insertText`.
+ WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText, mWidget);
+ insertTextEvent.mString = Some(str);
+ DispatchEvent(insertTextEvent);
+ return;
+ }
+
+ // Otherise, emulate an IME composition. This is our traditional behavior,
+ // but `beforeinput` events are not cancelable since they should be so for
+ // native IME limitation. So, this is now seriously imcompatible with the
+ // other browsers.
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "cannot continue handling composition after compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!DispatchCompositionCommitEvent(&str)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by compositioncommit event",
+ this));
+ return;
+ }
+
+ mMarkedRange = NSMakeRange(NSNotFound, 0);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString, NSRange& aSelectedRange,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "aAttrString=\"%s\", aSelectedRange={ location=%lu, length=%lu }, "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }, keyevent=%p, "
+ "keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If SetMarkedText() is called during handling a key press, that means that
+ // the key event caused this composition. So, keypress event shouldn't
+ // be dispatched later, let's mark the key event causing composition event.
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent(). On the other hand, if we're
+ // not in composition, the key event starts new composition. So, we
+ // need to mark the eKeyDown event as "processed by IME".
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+ }
+
+ if (Destroyed()) {
+ return;
+ }
+
+ // First, commit current composition with the latest composition string if the
+ // replacement range is different from marked range.
+ if (IsIMEComposing() && aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(MarkedRange(), *aReplacementRange)) {
+ AutoRestore<bool> ignoreIMECommit(mIgnoreIMECommit);
+ mIgnoreIMECommit = false;
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ mMarkedRange.length = str.Length();
+
+ if (!IsIMEComposing() && !str.IsEmpty()) {
+ // If there is no selection and replacement range is specified, set the
+ // range as selection.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+ // Set temporary selection range since OnSelectionChange is async.
+ mSelectedRange = *aReplacementRange;
+ if (NS_WARN_IF(!SetSelection(*aReplacementRange))) {
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ return;
+ }
+ }
+
+ mMarkedRange.location = SelectedRange().location;
+
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!str.IsEmpty()) {
+ if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionchange",
+ this));
+ }
+ return;
+ }
+
+ // If the composition string becomes empty string, we should commit
+ // current composition.
+ if (!DispatchCompositionCommitEvent(&EmptyString())) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by compositioncommit event",
+ this));
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NSAttributedString* IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange,
+ NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p, Destroyed()=%s",
+ this, static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange, TrueOrFalse(Destroyed())));
+
+ if (aActualRange) {
+ *aActualRange = NSMakeRange(NSNotFound, 0);
+ }
+
+ if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) {
+ return nil;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If we're in composing, the queried range may be in the composition string.
+ // In such case, we should use mIMECompositionString since if the composition
+ // string is handled by a remote process, the content cache may be out of
+ // date.
+ // XXX Should we set composition string attributes? Although, Blink claims
+ // that some attributes of marked text are supported, but they return
+ // just marked string without any style. So, let's keep current behavior
+ // at least for now.
+ NSUInteger compositionLength = mIMECompositionString ? [mIMECompositionString length] : 0;
+ if (mIMECompositionStart != UINT32_MAX && aRange.location >= mIMECompositionStart &&
+ aRange.location + aRange.length <= mIMECompositionStart + compositionLength) {
+ NSRange range = NSMakeRange(aRange.location - mIMECompositionStart, aRange.length);
+ NSString* nsstr = [mIMECompositionString substringWithRange:range];
+ NSMutableAttributedString* result =
+ [[[NSMutableAttributedString alloc] initWithString:nsstr attributes:nil] autorelease];
+ // XXX We cannot return font information in this case. However, this
+ // case must occur only when IME tries to confirm if composing string
+ // is handled as expected.
+ if (aActualRange) {
+ *aActualRange = aRange;
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(nsstr, str);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "computed with mIMECompositionString (result string=\"%s\")",
+ this, NS_ConvertUTF16toUTF8(str).get()));
+ }
+ return result;
+ }
+
+ nsAutoString str;
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextContentEvent.InitForQueryTextContent(startOffset, aRange.length, options);
+ queryTextContentEvent.RequestFontRanges();
+ DispatchEvent(queryTextContentEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "queryTextContentEvent={ mReply=%s }",
+ this, ToString(queryTextContentEvent.mReply).c_str()));
+
+ if (queryTextContentEvent.Failed()) {
+ return nil;
+ }
+
+ // We don't set vertical information at this point. If required,
+ // OS will calls drawsVerticallyForCharacterAtIndex.
+ NSMutableAttributedString* result = nsCocoaUtils::GetNSMutableAttributedString(
+ queryTextContentEvent.mReply->DataRef(), queryTextContentEvent.mReply->mFontRanges, false,
+ mWidget->BackingScaleFactor());
+ if (aActualRange) {
+ *aActualRange = MakeNSRangeFrom(queryTextContentEvent.mReply->mOffsetAndData);
+ }
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+bool IMEInputHandler::HasMarkedText() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::HasMarkedText, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0);
+}
+
+NSRange IMEInputHandler::MarkedRange() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MarkedRange, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (!HasMarkedText()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return mMarkedRange;
+}
+
+NSRange IMEInputHandler::SelectedRange() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ "
+ "location=%lu, length=%lu }",
+ this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(mSelectedRange.location),
+ static_cast<unsigned long>(mSelectedRange.length)));
+
+ if (Destroyed()) {
+ return mSelectedRange;
+ }
+
+ if (mSelectedRange.location != NSNotFound) {
+ MOZ_ASSERT(mIMEHasFocus);
+ return mSelectedRange;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, mWidget);
+ DispatchEvent(querySelectedTextEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, querySelectedTextEvent={ mReply=%s }", this,
+ ToString(querySelectedTextEvent.mReply).c_str()));
+
+ if (querySelectedTextEvent.Failed()) {
+ return mSelectedRange;
+ }
+
+ mWritingMode = querySelectedTextEvent.mReply->WritingModeRef();
+ mRangeForWritingMode = MakeNSRangeFrom(querySelectedTextEvent.mReply->mOffsetAndData);
+
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+
+ return mRangeForWritingMode;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(mSelectedRange);
+}
+
+bool IMEInputHandler::DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ if (mRangeForWritingMode.location == NSNotFound) {
+ // Update cached writing-mode value for the current selection.
+ SelectedRange();
+ }
+
+ if (aCharIndex < mRangeForWritingMode.location ||
+ aCharIndex > mRangeForWritingMode.location + mRangeForWritingMode.length) {
+ // It's not clear to me whether this ever happens in practice, but if an
+ // IME ever wants to query writing mode at an offset outside the current
+ // selection, the writing-mode value may not be correct for the index.
+ // In that case, use FirstRectForCharacterRange to get a fresh value.
+ // This does more work than strictly necessary (we don't need the rect here),
+ // but should be a rare case.
+ NS_WARNING("DrawsVerticallyForCharacterAtIndex not using cached writing mode");
+ NSRange range = NSMakeRange(aCharIndex, 1);
+ NSRange actualRange;
+ FirstRectForCharacterRange(range, &actualRange);
+ }
+
+ return mWritingMode.IsVertical();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+NSRect IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange, NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p }",
+ this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange));
+
+ // XXX this returns first character rect or caret rect, it is limitation of
+ // now. We need more work for returns first line rect. But current
+ // implementation is enough for IMEs.
+
+ NSRect rect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
+ NSRange actualRange = NSMakeRange(NSNotFound, 0);
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+ if (Destroyed() || aRange.location == NSNotFound) {
+ return rect;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ LayoutDeviceIntRect r;
+ bool useCaretRect = (aRange.length == 0);
+ if (!useCaretRect) {
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextRectEvent.InitForQueryTextRect(startOffset, 1, options);
+ DispatchEvent(queryTextRectEvent);
+ if (queryTextRectEvent.Succeeded()) {
+ r = queryTextRectEvent.mReply->mRect;
+ actualRange = MakeNSRangeFrom(queryTextRectEvent.mReply->mOffsetAndData);
+ mWritingMode = queryTextRectEvent.mReply->WritingModeRef();
+ mRangeForWritingMode = actualRange;
+ } else {
+ useCaretRect = true;
+ }
+ }
+
+ if (useCaretRect) {
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryCaretRectEvent.InitForQueryCaretRect(startOffset, options);
+ DispatchEvent(queryCaretRectEvent);
+ if (queryCaretRectEvent.Failed()) {
+ return rect;
+ }
+ r = queryCaretRectEvent.mReply->mRect;
+ r.width = 0;
+ actualRange.location = queryCaretRectEvent.mReply->StartOffset();
+ actualRange.length = 0;
+ }
+
+ nsIWidget* rootWidget = mWidget->GetTopLevelWidget();
+ NSWindow* rootWindow = static_cast<NSWindow*>(rootWidget->GetNativeData(NS_NATIVE_WINDOW));
+ NSView* rootView = static_cast<NSView*>(rootWidget->GetNativeData(NS_NATIVE_WIDGET));
+ if (!rootWindow || !rootView) {
+ return rect;
+ }
+ rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, mWidget->BackingScaleFactor());
+ rect = [rootView convertRect:rect toView:nil];
+ rect.origin = nsCocoaUtils::ConvertPointToScreen(rootWindow, rect.origin);
+
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, "
+ "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, "
+ "actualRange={ location=%lu, length=%lu }",
+ this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y, rect.size.width,
+ rect.size.height, static_cast<unsigned long>(actualRange.location),
+ static_cast<unsigned long>(actualRange.length)));
+
+ return rect;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRect(0.0, 0.0, 0.0, 0.0));
+}
+
+NSUInteger IMEInputHandler::CharacterIndexForPoint(NSPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CharacterIndexForPoint, aPoint={ x=%f, y=%f }", this, aPoint.x,
+ aPoint.y));
+
+ NSWindow* mainWindow = [NSApp mainWindow];
+ if (!mWidget || !mainWindow) {
+ return NSNotFound;
+ }
+
+ WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, mWidget);
+ NSPoint ptInWindow = nsCocoaUtils::ConvertPointFromScreen(mainWindow, aPoint);
+ NSPoint ptInView = [mView convertPoint:ptInWindow fromView:nil];
+ queryCharAtPointEvent.mRefPoint.x =
+ static_cast<int32_t>(ptInView.x) * mWidget->BackingScaleFactor();
+ queryCharAtPointEvent.mRefPoint.y =
+ static_cast<int32_t>(ptInView.y) * mWidget->BackingScaleFactor();
+ mWidget->DispatchWindowEvent(queryCharAtPointEvent);
+ if (queryCharAtPointEvent.Failed() || queryCharAtPointEvent.DidNotFindChar() ||
+ queryCharAtPointEvent.mReply->StartOffset() >= static_cast<uint32_t>(NSNotFound)) {
+ return NSNotFound;
+ }
+
+ return queryCharAtPointEvent.mReply->StartOffset();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNotFound);
+}
+
+extern "C" {
+extern NSString* NSTextInputReplacementRangeAttributeName;
+}
+
+NSArray* IMEInputHandler::GetValidAttributesForMarkedText() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::GetValidAttributesForMarkedText", this));
+
+ // Return same attributes as Chromium (see render_widget_host_view_mac.mm)
+ // because most IMEs must be tested with Safari (OS default) and Chrome
+ // (having most market share). Therefore, we need to follow their behavior.
+ // XXX It might be better to reuse an array instance for this result because
+ // this may be called a lot. Note that Chromium does so.
+ return [NSArray arrayWithObjects:NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName,
+ NSMarkedClauseSegmentAttributeName,
+ NSTextInputReplacementRangeAttributeName, nil];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #2
+ *
+ ******************************************************************************/
+
+IMEInputHandler::IMEInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : TextInputHandlerBase(aWidget, aNativeView),
+ mPendingMethods(0),
+ mIMECompositionString(nullptr),
+ mIMECompositionStart(UINT32_MAX),
+ mRangeForWritingMode(),
+ mIsIMEComposing(false),
+ mIsDeadKeyComposing(false),
+ mIsIMEEnabled(true),
+ mIsASCIICapableOnly(false),
+ mIgnoreIMECommit(false),
+ mIMEHasFocus(false) {
+ InitStaticMembers();
+
+ mMarkedRange.location = NSNotFound;
+ mMarkedRange.length = 0;
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+}
+
+IMEInputHandler::~IMEInputHandler() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (sFocusedIMEHandler == this) {
+ sFocusedIMEHandler = nullptr;
+ }
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+}
+
+void IMEInputHandler::OnFocusChangeInGecko(bool aFocus) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnFocusChangeInGecko, aFocus=%s, Destroyed()=%s, "
+ "sFocusedIMEHandler=%p",
+ this, TrueOrFalse(aFocus), TrueOrFalse(Destroyed()), sFocusedIMEHandler));
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = aFocus;
+
+ // This is called when the native focus is changed and when the native focus
+ // isn't changed but the focus is changed in Gecko.
+ if (!aFocus) {
+ if (sFocusedIMEHandler == this) sFocusedIMEHandler = nullptr;
+ return;
+ }
+
+ sFocusedIMEHandler = this;
+
+ // We need to notify IME of focus change in Gecko as native focus change
+ // because the window level of the focused element in Gecko may be changed.
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ ResetTimer();
+}
+
+bool IMEInputHandler::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnDestroyWidget, aDestroyingWidget=%p, "
+ "sFocusedIMEHandler=%p, IsIMEComposing()=%s",
+ this, aDestroyingWidget, sFocusedIMEHandler, TrueOrFalse(IsIMEComposing())));
+
+ // If we're not focused, the focused IMEInputHandler may have been
+ // created by another widget/nsChildView.
+ if (sFocusedIMEHandler && sFocusedIMEHandler != this) {
+ sFocusedIMEHandler->OnDestroyWidget(aDestroyingWidget);
+ }
+
+ if (!TextInputHandlerBase::OnDestroyWidget(aDestroyingWidget)) {
+ return false;
+ }
+
+ if (IsIMEComposing()) {
+ // If our view is in the composition, we should clean up it.
+ CancelIMEComposition();
+ }
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = false;
+
+ return true;
+}
+
+void IMEInputHandler::SendCommittedText(NSString* aString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(mWidget, );
+ // XXX We should send the string without mView.
+ if (!mView) {
+ return;
+ }
+
+ NSAttributedString* attrStr = [[NSAttributedString alloc] initWithString:aString];
+ if ([mView conformsToProtocol:@protocol(NSTextInputClient)]) {
+ NSObject<NSTextInputClient>* textInputClient = static_cast<NSObject<NSTextInputClient>*>(mView);
+ [textInputClient insertText:attrStr replacementRange:NSMakeRange(NSNotFound, 0)];
+ }
+
+ // Last resort. If we cannot retrieve NSTextInputProtocol from mView
+ // or blocking to call our InsertText(), we should call InsertText()
+ // directly to commit composition forcibly.
+ if (mIsIMEComposing) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, trying to insert text directly "
+ "due to IME not calling our InsertText()",
+ this));
+ static_cast<TextInputHandler*>(this)->InsertText(attrStr);
+ MOZ_ASSERT(!mIsIMEComposing);
+ }
+
+ [attrStr release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::KillIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::KillIMEComposition, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s, "
+ "Destroyed()=%s, IsFocused()=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing), TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused())));
+
+ if (Destroyed() || NS_WARN_IF(!mView)) {
+ return;
+ }
+
+ NSTextInputContext* inputContext = [mView inputContext];
+ if (NS_WARN_IF(!inputContext)) {
+ return;
+ }
+ [inputContext discardMarkedText];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CommitIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CommitIMEComposition, mIMECompositionString=%s", this,
+ GetCharacters(mIMECompositionString)));
+
+ // If this is called before dispatching eCompositionStart, IsIMEComposing()
+ // returns false. Even in such case, we need to commit composition *in*
+ // IME if this is called by preceding eKeyDown event of eCompositionStart.
+ // So, we need to call KillIMEComposition() even when IsIMEComposing()
+ // returns false.
+ KillIMEComposition();
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to finish the our composition too.
+ SendCommittedText(mIMECompositionString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CancelIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsIMEComposing()) return;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CancelIMEComposition, mIMECompositionString=%s", this,
+ GetCharacters(mIMECompositionString)));
+
+ // For canceling the current composing, we need to ignore the param of
+ // insertText. But this code is ugly...
+ mIgnoreIMECommit = true;
+ KillIMEComposition();
+ mIgnoreIMECommit = false;
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to kill the our composition too.
+ SendCommittedText(@"");
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::IsFocused() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+ NSWindow* window = [mView window];
+ NS_ENSURE_TRUE(window, false);
+ return [window firstResponder] == mView && [window isKeyWindow] &&
+ [[NSApplication sharedApplication] isActive];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::IsIMEOpened() {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ return tis.IsOpenedIMEMode();
+}
+
+void IMEInputHandler::SetASCIICapableOnly(bool aASCIICapableOnly) {
+ if (aASCIICapableOnly == mIsASCIICapableOnly) return;
+
+ CommitIMEComposition();
+ mIsASCIICapableOnly = aASCIICapableOnly;
+ SyncASCIICapableOnly();
+}
+
+void IMEInputHandler::EnableIME(bool aEnableIME) {
+ if (aEnableIME == mIsIMEEnabled) return;
+
+ CommitIMEComposition();
+ mIsIMEEnabled = aEnableIME;
+}
+
+void IMEInputHandler::SetIMEOpenState(bool aOpenIME) {
+ if (!IsFocused() || IsIMEOpened() == aOpenIME) return;
+
+ if (!aOpenIME) {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentASCIICapableInputSource();
+ tis.Select();
+ return;
+ }
+
+ // If we know the latest IME opened mode, we should select it.
+ if (sLatestIMEOpenedModeInputSourceID) {
+ TISInputSourceWrapper tis;
+ tis.InitByInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ tis.Select();
+ return;
+ }
+
+ // XXX If the current input source is a mode of IME, we should turn on it,
+ // but we haven't found such way...
+
+ // Finally, we should refer the system locale but this is a little expensive,
+ // we shouldn't retry this (if it was succeeded, we already set
+ // sLatestIMEOpenedModeInputSourceID at that time).
+ static bool sIsPrefferredIMESearched = false;
+ if (sIsPrefferredIMESearched) return;
+ sIsPrefferredIMESearched = true;
+ OpenSystemPreferredLanguageIME();
+}
+
+void IMEInputHandler::OpenSystemPreferredLanguageIME() {
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this));
+
+ CFArrayRef langList = ::CFLocaleCopyPreferredLanguages();
+ if (!langList) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, langList is NULL", this));
+ return;
+ }
+ CFIndex count = ::CFArrayGetCount(langList);
+ for (CFIndex i = 0; i < count; i++) {
+ CFLocaleRef locale = ::CFLocaleCreate(
+ kCFAllocatorDefault, static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, i)));
+ if (!locale) {
+ continue;
+ }
+
+ bool changed = false;
+ CFStringRef lang = static_cast<CFStringRef>(::CFLocaleGetValue(locale, kCFLocaleLanguageCode));
+ NS_ASSERTION(lang, "lang is null");
+ if (lang) {
+ TISInputSourceWrapper tis;
+ tis.InitByLanguage(lang);
+ if (tis.IsOpenedIMEMode()) {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFStringRef foundTIS;
+ tis.GetInputSourceID(foundTIS);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, "
+ "foundTIS=%s, lang=%s",
+ this, GetCharacters(foundTIS), GetCharacters(lang)));
+ }
+ tis.Select();
+ changed = true;
+ }
+ }
+ ::CFRelease(locale);
+ if (changed) {
+ break;
+ }
+ }
+ ::CFRelease(langList);
+}
+
+void IMEInputHandler::OnSelectionChange(const IMENotification& aIMENotification) {
+ MOZ_ASSERT(aIMENotification.mSelectionChangeData.IsInitialized());
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::OnSelectionChange", this));
+
+ if (!aIMENotification.mSelectionChangeData.HasRange()) {
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+ mRangeForWritingMode.location = NSNotFound;
+ mRangeForWritingMode.length = 0;
+ return;
+ }
+
+ mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode();
+ mRangeForWritingMode = NSMakeRange(aIMENotification.mSelectionChangeData.mOffset,
+ aIMENotification.mSelectionChangeData.Length());
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+}
+
+void IMEInputHandler::OnLayoutChange() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsFocused()) {
+ return;
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ [inputContext invalidateCharacterCoordinates];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::OnHandleEvent(NSEvent* aEvent) {
+ if (!IsFocused()) {
+ return false;
+ }
+
+ bool allowConsumeEvent = true;
+ if (nsCocoaFeatures::OnCatalinaOrLater() && !IsIMEComposing()) {
+ // Hack for bug of Korean IMEs on Catalina (10.15).
+ // If we are inactivated during composition, active Korean IME keeps
+ // consuming all mousedown events of any mouse buttons. So, we should
+ // allow Korean IMEs to handle mousedown events only when there is
+ // composition string.
+ // List of ID of Korean IME:
+ // * com.apple.inputmethod.Korean.2SetKorean
+ // * com.apple.inputmethod.Korean.3SetKorean
+ // * com.apple.inputmethod.Korean.390Sebulshik
+ // * com.apple.inputmethod.Korean.GongjinCheongRomaja
+ // * com.apple.inputmethod.Korean.HNCRomaja
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ nsAutoString inputSourceID;
+ tis.GetInputSourceID(inputSourceID);
+ allowConsumeEvent = !StringBeginsWith(inputSourceID, u"com.apple.inputmethod.Korean."_ns);
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ return [inputContext handleEvent:aEvent] && allowConsumeEvent;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase implementation
+ *
+ ******************************************************************************/
+
+int32_t TextInputHandlerBase::sSecureEventInputCount = 0;
+
+NS_IMPL_ISUPPORTS(TextInputHandlerBase, TextEventDispatcherListener, nsISupportsWeakReference)
+
+TextInputHandlerBase::TextInputHandlerBase(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : mWidget(aWidget), mDispatcher(aWidget->GetTextEventDispatcher()) {
+ gHandlerInstanceCount++;
+ mView = [aNativeView retain];
+}
+
+TextInputHandlerBase::~TextInputHandlerBase() {
+ [mView release];
+ if (--gHandlerInstanceCount == 0) {
+ TISInputSourceWrapper::Shutdown();
+ }
+}
+
+bool TextInputHandlerBase::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandlerBase::OnDestroyWidget, "
+ "aDestroyingWidget=%p, mWidget=%p",
+ this, aDestroyingWidget, mWidget));
+
+ if (aDestroyingWidget != mWidget) {
+ return false;
+ }
+
+ mWidget = nullptr;
+ mDispatcher = nullptr;
+ return true;
+}
+
+bool TextInputHandlerBase::DispatchEvent(WidgetGUIEvent& aEvent) {
+ return mWidget->DispatchWindowEvent(aEvent);
+}
+
+void TextInputHandlerBase::InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString) {
+ NS_ASSERTION(aNativeKeyEvent, "aNativeKeyEvent must not be NULL");
+
+ if (mKeyboardOverride.mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(mKeyboardOverride.mKeyboardLayout, true);
+ tis.InitKeyEvent(aNativeKeyEvent, aKeyEvent, aIsProcessedByIME, aInsertString);
+ return;
+ }
+ TISInputSourceWrapper::CurrentInputSource().InitKeyEvent(aNativeKeyEvent, aKeyEvent,
+ aIsProcessedByIME, aInsertString);
+}
+
+nsresult TextInputHandlerBase::SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ uint32_t modifierFlags = nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
+ static_cast<nsIWidget::Modifiers>(aModifierFlags));
+ NSInteger windowNumber = [[mView window] windowNumber];
+ bool sendFlagsChangedEvent = IsModifierKey(aNativeKeyCode);
+ NSEventType eventType = sendFlagsChangedEvent ? NSEventTypeFlagsChanged : NSEventTypeKeyDown;
+ NSEvent* downEvent = [NSEvent keyEventWithType:eventType
+ location:NSMakePoint(0, 0)
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:windowNumber
+ context:nil
+ characters:nsCocoaUtils::ToNSString(aCharacters)
+ charactersIgnoringModifiers:nsCocoaUtils::ToNSString(aUnmodifiedCharacters)
+ isARepeat:NO
+ keyCode:aNativeKeyCode];
+
+ NSEvent* upEvent = sendFlagsChangedEvent
+ ? nil
+ : nsCocoaUtils::MakeNewCocoaEventWithType(NSEventTypeKeyUp, downEvent);
+
+ if (downEvent && (sendFlagsChangedEvent || upEvent)) {
+ KeyboardLayoutOverride currentLayout = mKeyboardOverride;
+ mKeyboardOverride.mKeyboardLayout = aNativeKeyboardLayout;
+ mKeyboardOverride.mOverrideEnabled = true;
+ [NSApp sendEvent:downEvent];
+ if (upEvent) {
+ [NSApp sendEvent:upEvent];
+ }
+ // processKeyDownEvent and keyUp block exceptions so we're sure to
+ // reach here to restore mKeyboardOverride
+ mKeyboardOverride = currentLayout;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NSInteger TextInputHandlerBase::GetWindowLevel() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandlerBase::GetWindowLevel, Destryoed()=%s",
+ this, TrueOrFalse(Destroyed())));
+
+ if (Destroyed()) {
+ return NSNormalWindowLevel;
+ }
+
+ // When an <input> element on a XUL <panel> is focused, the actual focused view
+ // is the panel's parent view (mView). But the editor is displayed on the
+ // popped-up widget's view (editorView). We want the latter's window level.
+ NSView<mozView>* editorView = mWidget->GetEditorView();
+ NS_ENSURE_TRUE(editorView, NSNormalWindowLevel);
+ NSInteger windowLevel = [[editorView window] level];
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%lX)", this,
+ GetWindowLevelName(windowLevel), static_cast<unsigned long>(windowLevel)));
+
+ return windowLevel;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNormalWindowLevel);
+}
+
+NS_IMETHODIMP
+TextInputHandlerBase::AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Don't try to replace a native event if one already exists.
+ // OS X doesn't have an OS modifier, can't make a native event.
+ if (aKeyEvent.mNativeKeyEvent || aKeyEvent.mModifiers & MODIFIER_OS) {
+ return NS_OK;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::AttachNativeKeyEvent, key=0x%X, char=0x%X, "
+ "mod=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, aKeyEvent.mModifiers));
+
+ NSInteger windowNumber = [[mView window] windowNumber];
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ aKeyEvent.mNativeKeyEvent =
+ nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aKeyEvent, windowNumber, context);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool TextInputHandlerBase::SetSelection(NSRange& aRange) {
+ MOZ_ASSERT(!Destroyed());
+
+ RefPtr<TextInputHandlerBase> kungFuDeathGrip(this);
+ WidgetSelectionEvent selectionEvent(true, eSetSelection, mWidget);
+ selectionEvent.mOffset = aRange.location;
+ selectionEvent.mLength = aRange.length;
+ selectionEvent.mReversed = false;
+ selectionEvent.mExpandToClusterBoundary = false;
+ DispatchEvent(selectionEvent);
+ NS_ENSURE_TRUE(selectionEvent.mSucceeded, false);
+ return !Destroyed();
+}
+
+/* static */ bool TextInputHandlerBase::IsPrintableChar(char16_t aChar) {
+ return (aChar >= 0x20 && aChar <= 0x7E) || aChar >= 0xA0;
+}
+
+/* static */ bool TextInputHandlerBase::IsSpecialGeckoKey(UInt32 aNativeKeyCode) {
+ // this table is used to determine which keys are special and should not
+ // generate a charCode
+ switch (aNativeKeyCode) {
+ // modifiers - we don't get separate events for these yet
+ case kVK_Escape:
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_CapsLock:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_ANSI_KeypadClear:
+ case kVK_Function:
+
+ // function keys
+ case kVK_F1:
+ case kVK_F2:
+ case kVK_F3:
+ case kVK_F4:
+ case kVK_F5:
+ case kVK_F6:
+ case kVK_F7:
+ case kVK_F8:
+ case kVK_F9:
+ case kVK_F10:
+ case kVK_F11:
+ case kVK_F12:
+ case kVK_PC_Pause:
+ case kVK_PC_ScrollLock:
+ case kVK_PC_PrintScreen:
+ case kVK_F16:
+ case kVK_F17:
+ case kVK_F18:
+ case kVK_F19:
+
+ case kVK_PC_Insert:
+ case kVK_PC_Delete:
+ case kVK_Tab:
+ case kVK_PC_Backspace:
+ case kVK_PC_ContextMenu:
+
+ case kVK_JIS_Eisu:
+ case kVK_JIS_Kana:
+
+ case kVK_Home:
+ case kVK_End:
+ case kVK_PageUp:
+ case kVK_PageDown:
+ case kVK_LeftArrow:
+ case kVK_RightArrow:
+ case kVK_UpArrow:
+ case kVK_DownArrow:
+ case kVK_Return:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Powerbook_KeypadEnter:
+ return true;
+ }
+ return false;
+}
+
+/* static */ bool TextInputHandlerBase::IsNormalCharInputtingEvent(NSEvent* aNativeEvent) {
+ if ([aNativeEvent type] != NSEventTypeKeyDown && [aNativeEvent type] != NSEventTypeKeyUp) {
+ return false;
+ }
+ nsAutoString nativeChars;
+ nsCocoaUtils::GetStringForNSString([aNativeEvent characters], nativeChars);
+
+ // this is not character inputting event, simply.
+ if (nativeChars.IsEmpty() || ([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ return false;
+ }
+ return !IsControlChar(nativeChars[0]);
+}
+
+/* static */ bool TextInputHandlerBase::IsModifierKey(UInt32 aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_CapsLock:
+ case kVK_RightCommand:
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ case kVK_Function:
+ return true;
+ }
+ return false;
+}
+
+/* static */ void TextInputHandlerBase::EnableSecureEventInput() {
+ sSecureEventInputCount++;
+ ::EnableSecureEventInput();
+}
+
+/* static */ void TextInputHandlerBase::DisableSecureEventInput() {
+ if (!sSecureEventInputCount) {
+ return;
+ }
+ sSecureEventInputCount--;
+ ::DisableSecureEventInput();
+}
+
+/* static */ bool TextInputHandlerBase::IsSecureEventInputEnabled() {
+ // sSecureEventInputCount is our mechanism to track when Secure Event Input
+ // is enabled. Non-zero indicates we have enabled Secure Input. But
+ // zero does not mean that Secure Input is _disabled_ because another
+ // application may have enabled it. If the OS reports Secure Event
+ // Input is disabled though, a non-zero sSecureEventInputCount is an error.
+ NS_ASSERTION(
+ ::IsSecureEventInputEnabled() || 0 == sSecureEventInputCount,
+ "sSecureEventInputCount is not zero when the OS thinks SecureEventInput is disabled.");
+ return !!sSecureEventInputCount;
+}
+
+/* static */ void TextInputHandlerBase::EnsureSecureEventInputDisabled() {
+ while (sSecureEventInputCount) {
+ TextInputHandlerBase::DisableSecureEventInput();
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::KeyEventState implementation
+ *
+ ******************************************************************************/
+
+void TextInputHandlerBase::KeyEventState::InitKeyEvent(TextInputHandlerBase* aHandler,
+ WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aHandler);
+ MOZ_RELEASE_ASSERT(mKeyEvent);
+
+ NSEvent* nativeEvent = mKeyEvent;
+ if (!mInsertedString.IsEmpty()) {
+ nsAutoString unhandledString;
+ GetUnhandledString(unhandledString);
+ NSString* unhandledNSString = nsCocoaUtils::ToNSString(unhandledString);
+ // If the key event's some characters were already handled by
+ // InsertString() calls, we need to create a dummy event which doesn't
+ // include the handled characters.
+ nativeEvent = [NSEvent keyEventWithType:[mKeyEvent type]
+ location:[mKeyEvent locationInWindow]
+ modifierFlags:[mKeyEvent modifierFlags]
+ timestamp:[mKeyEvent timestamp]
+ windowNumber:[mKeyEvent windowNumber]
+ context:nil
+ characters:unhandledNSString
+ charactersIgnoringModifiers:[mKeyEvent charactersIgnoringModifiers]
+ isARepeat:[mKeyEvent isARepeat]
+ keyCode:[mKeyEvent keyCode]];
+ }
+
+ aKeyEvent.mUniqueId = mUniqueId;
+ aHandler->InitKeyEvent(nativeEvent, aKeyEvent, aIsProcessedByIME, mInsertString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandlerBase::KeyEventState::GetUnhandledString(nsAString& aUnhandledString) const {
+ aUnhandledString.Truncate();
+ if (NS_WARN_IF(!mKeyEvent)) {
+ return;
+ }
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mKeyEvent characters], characters);
+ if (characters.IsEmpty()) {
+ return;
+ }
+ if (mInsertedString.IsEmpty()) {
+ aUnhandledString = characters;
+ return;
+ }
+
+ // The insertes string must match with the start of characters.
+ MOZ_ASSERT(StringBeginsWith(characters, mInsertedString));
+
+ aUnhandledString = nsDependentSubstring(characters, mInsertedString.Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::AutoInsertStringClearer implementation
+ *
+ ******************************************************************************/
+
+TextInputHandlerBase::AutoInsertStringClearer::~AutoInsertStringClearer() {
+ if (mState && mState->mInsertString) {
+ // If inserting string is a part of characters of the event,
+ // we should record it as inserted string.
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mState->mKeyEvent characters], characters);
+ nsAutoString insertedString(mState->mInsertedString);
+ insertedString += *mState->mInsertString;
+ if (StringBeginsWith(characters, insertedString)) {
+ mState->mInsertedString = insertedString;
+ }
+ }
+ if (mState) {
+ mState->mInsertString = nullptr;
+ }
+}
+
+#undef MOZ_LOG_KEY_OR_IME
diff --git a/widget/cocoa/TextRecognition.mm b/widget/cocoa/TextRecognition.mm
new file mode 100644
index 0000000000..1c962a2c05
--- /dev/null
+++ b/widget/cocoa/TextRecognition.mm
@@ -0,0 +1,109 @@
+/* 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 <Vision/Vision.h>
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/ErrorResult.h"
+#include "ErrorList.h"
+#include "nsClipboard.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/MacStringHelpers.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/widget/TextRecognition.h"
+#include "mozilla/dom/PContent.h"
+
+namespace mozilla::widget {
+
+auto TextRecognition::DoFindText(gfx::DataSourceSurface& aSurface,
+ const nsTArray<nsCString>& aLanguages) -> RefPtr<NativePromise> {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+ if (@available(macOS 10.15, *)) {
+ // TODO - Is this the most efficient path? Maybe we can write a new
+ // CreateCGImageFromXXX that enables more efficient marshalling of the data.
+ CGImageRef imageRef = NULL;
+ nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(&aSurface, &imageRef);
+ if (NS_FAILED(rv) || !imageRef) {
+ return NativePromise::CreateAndReject("Failed to create CGImage"_ns, __func__);
+ }
+
+ auto promise = MakeRefPtr<NativePromise::Private>(__func__);
+
+ NSMutableArray* recognitionLanguages = [[NSMutableArray alloc] init];
+ for (const auto& locale : aLanguages) {
+ [recognitionLanguages addObject:nsCocoaUtils::ToNSString(locale)];
+ }
+
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ __func__,
+ [promise, imageRef, recognitionLanguages] {
+ auto unrefImage = MakeScopeExit([&] {
+ ::CGImageRelease(imageRef);
+ [recognitionLanguages release];
+ });
+
+ dom::TextRecognitionResult result;
+ dom::TextRecognitionResult* pResult = &result;
+
+ // Define the request to use, which also handles the result. It will be run below
+ // directly in this thread. After creating this request.
+ VNRecognizeTextRequest* textRecognitionRequest = [[VNRecognizeTextRequest alloc]
+ initWithCompletionHandler:^(VNRequest* _Nonnull request,
+ NSError* _Nullable error) {
+ NSArray<VNRecognizedTextObservation*>* observations = request.results;
+
+ [observations
+ enumerateObjectsUsingBlock:^(VNRecognizedTextObservation* _Nonnull obj,
+ NSUInteger idx, BOOL* _Nonnull stop) {
+ // Requests the n top candidates for a recognized text string.
+ VNRecognizedText* recognizedText = [obj topCandidates:1].firstObject;
+
+ // https://developer.apple.com/documentation/vision/vnrecognizedtext?language=objc
+ auto& quad = *pResult->quads().AppendElement();
+ CopyCocoaStringToXPCOMString(recognizedText.string, quad.string());
+ quad.confidence() = recognizedText.confidence;
+
+ auto ToImagePoint = [](CGPoint aPoint) -> ImagePoint {
+ return {static_cast<float>(aPoint.x), static_cast<float>(aPoint.y)};
+ };
+ *quad.points().AppendElement() = ToImagePoint(obj.bottomLeft);
+ *quad.points().AppendElement() = ToImagePoint(obj.topLeft);
+ *quad.points().AppendElement() = ToImagePoint(obj.topRight);
+ *quad.points().AppendElement() = ToImagePoint(obj.bottomRight);
+ }];
+ }];
+
+ textRecognitionRequest.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
+ textRecognitionRequest.recognitionLanguages = recognitionLanguages;
+ textRecognitionRequest.usesLanguageCorrection = true;
+
+ // Send out the request. This blocks execution of this thread with an expensive
+ // CPU call.
+ NSError* error = nil;
+ VNImageRequestHandler* requestHandler =
+ [[[VNImageRequestHandler alloc] initWithCGImage:imageRef
+ options:@{}] autorelease];
+
+ [requestHandler performRequests:@[ textRecognitionRequest ] error:&error];
+ if (error != nil) {
+ promise->Reject(
+ nsPrintfCString("Failed to perform text recognition request (%ld)\n",
+ error.code),
+ __func__);
+ } else {
+ promise->Resolve(std::move(result), __func__);
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return promise;
+ } else {
+ return NativePromise::CreateAndReject("Text recognition is not available"_ns, __func__);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+} // namespace mozilla::widget
diff --git a/widget/cocoa/VibrancyManager.h b/widget/cocoa/VibrancyManager.h
new file mode 100644
index 0000000000..b85b8b1e80
--- /dev/null
+++ b/widget/cocoa/VibrancyManager.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef VibrancyManager_h
+#define VibrancyManager_h
+
+#include "mozilla/Assertions.h"
+#include "nsClassHashtable.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+#include "ViewRegion.h"
+
+#import <Foundation/NSGeometry.h>
+
+@class NSColor;
+@class NSView;
+class nsChildView;
+
+namespace mozilla {
+
+enum class VibrancyType {
+ TOOLTIP,
+ MENU,
+ HIGHLIGHTED_MENUITEM,
+ SOURCE_LIST,
+ SOURCE_LIST_SELECTION,
+ ACTIVE_SOURCE_LIST_SELECTION
+};
+
+/**
+ * VibrancyManager takes care of updating the vibrant regions of a window.
+ * Vibrancy is a visual look that was introduced on OS X starting with 10.10.
+ * An app declares vibrant window regions to the window server, and the window
+ * server will display a blurred rendering of the screen contents from behind
+ * the window in these areas, behind the actual window contents. Consequently,
+ * the effect is only visible in areas where the window contents are not
+ * completely opaque. Usually this is achieved by clearing the background of
+ * the window prior to drawing in the vibrant areas. This is possible even if
+ * the window is declared as opaque.
+ */
+class VibrancyManager {
+ public:
+ /**
+ * Create a new VibrancyManager instance and provide it with an NSView
+ * to attach NSVisualEffectViews to.
+ *
+ * @param aCoordinateConverter The nsChildView to use for converting
+ * nsIntRect device pixel coordinates into Cocoa NSRect coordinates. Must
+ * outlive this VibrancyManager instance.
+ * @param aContainerView The view that's going to be the superview of the
+ * NSVisualEffectViews which will be created for vibrant regions.
+ */
+ VibrancyManager(const nsChildView& aCoordinateConverter, NSView* aContainerView)
+ : mCoordinateConverter(aCoordinateConverter), mContainerView(aContainerView) {}
+
+ /**
+ * Update the placement of the NSVisualEffectViews inside the container
+ * NSView so that they cover aRegion, and create new NSVisualEffectViews
+ * or remove existing ones as needed.
+ * @param aType The vibrancy type to use in the region.
+ * @param aRegion The vibrant area, in device pixels.
+ * @return Whether the region changed.
+ */
+ bool UpdateVibrantRegion(VibrancyType aType, const LayoutDeviceIntRegion& aRegion);
+
+ bool HasVibrantRegions() { return !mVibrantRegions.IsEmpty(); }
+
+ LayoutDeviceIntRegion GetUnionOfVibrantRegions() const;
+
+ /**
+ * Create an NSVisualEffectView for the specified vibrancy type. The return
+ * value is not autoreleased. We return an object of type NSView* because we
+ * compile with an SDK that does not contain a definition for
+ * NSVisualEffectView.
+ * @param aIsContainer Whether this NSView will have child views. This value
+ * affects hit testing: Container views will pass through
+ * hit testing requests to their children, and leaf views
+ * will be transparent to hit testing.
+ */
+ static NSView* CreateEffectView(VibrancyType aType, BOOL aIsContainer = NO);
+
+ protected:
+ const nsChildView& mCoordinateConverter;
+ NSView* mContainerView;
+ nsClassHashtable<nsUint32HashKey, ViewRegion> mVibrantRegions;
+};
+
+} // namespace mozilla
+
+#endif // VibrancyManager_h
diff --git a/widget/cocoa/VibrancyManager.mm b/widget/cocoa/VibrancyManager.mm
new file mode 100644
index 0000000000..a193f15490
--- /dev/null
+++ b/widget/cocoa/VibrancyManager.mm
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "VibrancyManager.h"
+
+#import <objc/message.h>
+
+#include "nsChildView.h"
+#include "nsCocoaFeatures.h"
+#include "SDKDeclarations.h"
+
+using namespace mozilla;
+
+@interface MOZVibrantView : NSVisualEffectView {
+ VibrancyType mType;
+}
+- (instancetype)initWithFrame:(NSRect)aRect vibrancyType:(VibrancyType)aVibrancyType;
+@end
+
+@interface MOZVibrantLeafView : MOZVibrantView
+@end
+
+static NSAppearance* AppearanceForVibrancyType(VibrancyType aType) {
+ if (@available(macOS 10.14, *)) {
+ // Inherit the appearance from the window. If the window is using Dark Mode, the vibrancy
+ // will automatically be dark, too. This is available starting with macOS 10.14.
+ return nil;
+ }
+
+ // For 10.13 and below, a vibrant appearance name must be used. There is no system dark mode and
+ // no automatic adaptation to the window; all windows are light.
+ switch (aType) {
+ case VibrancyType::TOOLTIP:
+ case VibrancyType::MENU:
+ case VibrancyType::HIGHLIGHTED_MENUITEM:
+ case VibrancyType::SOURCE_LIST:
+ case VibrancyType::SOURCE_LIST_SELECTION:
+ case VibrancyType::ACTIVE_SOURCE_LIST_SELECTION:
+ return [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight];
+ }
+}
+
+static NSVisualEffectState VisualEffectStateForVibrancyType(VibrancyType aType) {
+ switch (aType) {
+ case VibrancyType::TOOLTIP:
+ case VibrancyType::MENU:
+ case VibrancyType::HIGHLIGHTED_MENUITEM:
+ // Tooltip and menu windows are never "key", so we need to tell the vibrancy effect to look
+ // active regardless of window state.
+ return NSVisualEffectStateActive;
+ default:
+ return NSVisualEffectStateFollowsWindowActiveState;
+ }
+}
+
+static NSVisualEffectMaterial VisualEffectMaterialForVibrancyType(VibrancyType aType,
+ BOOL* aOutIsEmphasized) {
+ switch (aType) {
+ case VibrancyType::TOOLTIP:
+ if (@available(macOS 10.14, *)) {
+ return (NSVisualEffectMaterial)NSVisualEffectMaterialToolTip;
+ } else {
+ return NSVisualEffectMaterialMenu;
+ }
+ case VibrancyType::MENU:
+ return NSVisualEffectMaterialMenu;
+ case VibrancyType::SOURCE_LIST:
+ return NSVisualEffectMaterialSidebar;
+ case VibrancyType::SOURCE_LIST_SELECTION:
+ return NSVisualEffectMaterialSelection;
+ case VibrancyType::HIGHLIGHTED_MENUITEM:
+ case VibrancyType::ACTIVE_SOURCE_LIST_SELECTION:
+ *aOutIsEmphasized = YES;
+ return NSVisualEffectMaterialSelection;
+ }
+}
+
+static BOOL HasVibrantForeground(VibrancyType aType) {
+ if (@available(macOS 10.14, *)) {
+ return NO;
+ }
+ return aType == VibrancyType::MENU;
+}
+
+@implementation MOZVibrantView
+
+- (instancetype)initWithFrame:(NSRect)aRect vibrancyType:(VibrancyType)aType {
+ self = [super initWithFrame:aRect];
+ mType = aType;
+
+ self.appearance = AppearanceForVibrancyType(mType);
+ self.state = VisualEffectStateForVibrancyType(mType);
+
+ BOOL isEmphasized = NO;
+ self.material = VisualEffectMaterialForVibrancyType(mType, &isEmphasized);
+ self.emphasized = isEmphasized;
+
+ return self;
+}
+
+// Don't override allowsVibrancy here, because this view may have subviews, and
+// returning YES from allowsVibrancy forces on foreground vibrancy for all
+// descendant views, which can have unintended effects.
+
+@end
+
+@implementation MOZVibrantLeafView
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ // This view must be transparent to mouse events.
+ return nil;
+}
+
+// MOZVibrantLeafView does not have subviews, so we can return YES here without
+// having unintended effects on other contents of the window.
+- (BOOL)allowsVibrancy {
+ return HasVibrantForeground(mType);
+}
+
+@end
+
+bool VibrancyManager::UpdateVibrantRegion(VibrancyType aType,
+ const LayoutDeviceIntRegion& aRegion) {
+ if (aRegion.IsEmpty()) {
+ return mVibrantRegions.Remove(uint32_t(aType));
+ }
+ auto& vr = *mVibrantRegions.GetOrInsertNew(uint32_t(aType));
+ return vr.UpdateRegion(aRegion, mCoordinateConverter, mContainerView, ^() {
+ return this->CreateEffectView(aType);
+ });
+}
+
+LayoutDeviceIntRegion VibrancyManager::GetUnionOfVibrantRegions() const {
+ LayoutDeviceIntRegion result;
+ for (const auto& region : mVibrantRegions.Values()) {
+ result.OrWith(region->Region());
+ }
+ return result;
+}
+
+/* static */ NSView* VibrancyManager::CreateEffectView(VibrancyType aType, BOOL aIsContainer) {
+ return aIsContainer ? [[MOZVibrantView alloc] initWithFrame:NSZeroRect vibrancyType:aType]
+ : [[MOZVibrantLeafView alloc] initWithFrame:NSZeroRect vibrancyType:aType];
+}
diff --git a/widget/cocoa/ViewRegion.h b/widget/cocoa/ViewRegion.h
new file mode 100644
index 0000000000..4c98fab882
--- /dev/null
+++ b/widget/cocoa/ViewRegion.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef ViewRegion_h
+#define ViewRegion_h
+
+#include "Units.h"
+#include "nsTArray.h"
+
+class nsChildView;
+
+@class NSView;
+
+namespace mozilla {
+
+/**
+ * Manages a set of NSViews to cover a LayoutDeviceIntRegion.
+ */
+class ViewRegion {
+ public:
+ ~ViewRegion();
+
+ mozilla::LayoutDeviceIntRegion Region() { return mRegion; }
+
+ /**
+ * Update the region.
+ * @param aRegion The new region.
+ * @param aCoordinateConverter The nsChildView to use for converting
+ * LayoutDeviceIntRect device pixel coordinates into Cocoa NSRect coordinates.
+ * @param aContainerView The view that's going to be the superview of the
+ * NSViews which will be created for this region.
+ * @param aViewCreationCallback A block that instantiates new NSViews.
+ * @return Whether or not the region changed.
+ */
+ bool UpdateRegion(const mozilla::LayoutDeviceIntRegion& aRegion,
+ const nsChildView& aCoordinateConverter, NSView* aContainerView,
+ NSView* (^aViewCreationCallback)());
+
+ /**
+ * Return an NSView from the region, if there is any.
+ */
+ NSView* GetAnyView() { return mViews.Length() > 0 ? mViews[0] : NULL; }
+
+ private:
+ mozilla::LayoutDeviceIntRegion mRegion;
+ nsTArray<NSView*> mViews;
+};
+
+} // namespace mozilla
+
+#endif // ViewRegion_h
diff --git a/widget/cocoa/ViewRegion.mm b/widget/cocoa/ViewRegion.mm
new file mode 100644
index 0000000000..c99785f956
--- /dev/null
+++ b/widget/cocoa/ViewRegion.mm
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ViewRegion.h"
+#import <Cocoa/Cocoa.h>
+
+#include "nsChildView.h"
+
+using namespace mozilla;
+
+ViewRegion::~ViewRegion() {
+ for (size_t i = 0; i < mViews.Length(); i++) {
+ [mViews[i] removeFromSuperview];
+ }
+}
+
+bool ViewRegion::UpdateRegion(const LayoutDeviceIntRegion& aRegion,
+ const nsChildView& aCoordinateConverter, NSView* aContainerView,
+ NSView* (^aViewCreationCallback)()) {
+ if (mRegion == aRegion) {
+ return false;
+ }
+
+ // We need to construct the required region using as many EffectViews
+ // as necessary. We try to update the geometry of existing views if
+ // possible, or create new ones or remove old ones if the number of
+ // rects in the region has changed.
+
+ nsTArray<NSView*> viewsToRecycle = std::move(mViews);
+ // The mViews array is now empty.
+
+ size_t i = 0;
+ for (auto iter = aRegion.RectIter(); !iter.Done() || i < viewsToRecycle.Length(); i++) {
+ if (!iter.Done()) {
+ NSView* view = nil;
+ NSRect rect = aCoordinateConverter.DevPixelsToCocoaPoints(iter.Get());
+ if (i < viewsToRecycle.Length()) {
+ view = viewsToRecycle[i];
+ } else {
+ view = aViewCreationCallback();
+ [aContainerView addSubview:view];
+
+ // Now that the view is in the view hierarchy, it'll be kept alive by
+ // its superview, so we can drop our reference.
+ [view release];
+ }
+ if (!NSEqualRects(rect, [view frame])) {
+ [view setFrame:rect];
+ }
+ [view setNeedsDisplay:YES];
+ mViews.AppendElement(view);
+ iter.Next();
+ } else {
+ // Our new region is made of fewer rects than the old region, so we can
+ // remove this view. We only have a weak reference to it, so removing it
+ // from the view hierarchy will release it.
+ [viewsToRecycle[i] removeFromSuperview];
+ }
+ }
+
+ mRegion = aRegion;
+ return true;
+}
diff --git a/widget/cocoa/WidgetTraceEvent.mm b/widget/cocoa/WidgetTraceEvent.mm
new file mode 100644
index 0000000000..54fe497e3b
--- /dev/null
+++ b/widget/cocoa/WidgetTraceEvent.mm
@@ -0,0 +1,79 @@
+/* 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/. */
+
+#include <Cocoa/Cocoa.h>
+#include "CustomCocoaEvents.h"
+#include <Foundation/NSAutoreleasePool.h>
+#include <mozilla/CondVar.h>
+#include <mozilla/Mutex.h>
+#include "mozilla/WidgetTraceEvent.h"
+
+using mozilla::CondVar;
+using mozilla::Mutex;
+using mozilla::MutexAutoLock;
+
+namespace {
+
+Mutex* sMutex = NULL;
+CondVar* sCondVar = NULL;
+bool sTracerProcessed = false;
+
+} // namespace
+
+namespace mozilla {
+
+bool InitWidgetTracing() {
+ sMutex = new Mutex("Event tracer thread mutex");
+ sCondVar = new CondVar(*sMutex, "Event tracer thread condvar");
+ return sMutex && sCondVar;
+}
+
+void CleanUpWidgetTracing() {
+ delete sMutex;
+ delete sCondVar;
+ sMutex = NULL;
+ sCondVar = NULL;
+}
+
+// This function is called from the main (UI) thread.
+void SignalTracerThread() {
+ if (!sMutex || !sCondVar) return;
+ MutexAutoLock lock(*sMutex);
+ if (!sTracerProcessed) {
+ sTracerProcessed = true;
+ sCondVar->Notify();
+ }
+}
+
+// This function is called from the background tracer thread.
+bool FireAndWaitForTracerEvent() {
+ MOZ_ASSERT(sMutex && sCondVar, "Tracing not initialized!");
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+ MutexAutoLock lock(*sMutex);
+ if (sTracerProcessed) {
+ // Things are out of sync. This is likely because we're in
+ // the middle of shutting down. Just return false and hope the
+ // tracer thread is quitting anyway.
+ return false;
+ }
+
+ // Post an application-defined event to the main thread's event queue
+ // and wait for it to get processed.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeTrace
+ data1:0
+ data2:0]
+ atStart:NO];
+ while (!sTracerProcessed) sCondVar->Wait();
+ sTracerProcessed = false;
+ [pool release];
+ return true;
+}
+
+} // namespace mozilla
diff --git a/widget/cocoa/components.conf b/widget/cocoa/components.conf
new file mode 100644
index 0000000000..199e5cf255
--- /dev/null
+++ b/widget/cocoa/components.conf
@@ -0,0 +1,168 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Headers = '/widget/cocoa/nsWidgetFactory.h',
+
+InitFunc = 'nsWidgetCocoaModuleCtor'
+UnloadFunc = 'nsWidgetCocoaModuleDtor'
+
+Classes = [
+ {
+ 'cid': '{49f428e8-baf9-4ba3-b1b0-7d2fd3abbcea}',
+ 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'],
+ 'interfaces': ['nsIClipboard'],
+ 'type': 'nsIClipboard',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'name': 'GfxInfo',
+ 'cid': '{d755a760-9f27-11df-0800-200c9a664242}',
+ 'contract_ids': ['@mozilla.org/gfx/info;1'],
+ 'type': 'mozilla::widget::GfxInfo',
+ 'headers': ['/widget/cocoa/GfxInfo.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{e5170091-c16b-492d-bf00-f45d72470553}',
+ 'contract_ids': ['@mozilla.org/parent/filepicker;1'],
+ 'type': 'nsFilePicker',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{b90f5fdd-c23e-4ad6-a10e-1da8ffe07799}',
+ 'contract_ids': ['@mozilla.org/parent/colorpicker;1'],
+ 'type': 'nsColorPicker',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}',
+ 'contract_ids': ['@mozilla.org/widget/appshell/mac;1'],
+ 'legacy_constructor': 'nsAppShellConstructor',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{15cc80a9-5329-4fcb-9a0b-c6cf1440ae51}',
+ 'contract_ids': ['@mozilla.org/parent/sound;1'],
+ 'type': 'nsSound',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/transferable;1'],
+ 'type': 'nsTransferable',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'],
+ 'type': 'nsHTMLFormatConverter',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}',
+ 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'],
+ 'type': 'nsClipboardHelper',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{9a155bb2-2b67-45de-83e3-13a9dacf8336}',
+ 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'],
+ 'type': 'nsDragService',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{f0ddedd7-e8d5-4f95-a5b4-0f48f1741b36}',
+ 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'],
+ 'type': 'mozilla::widget::ScreenManager',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ 'singleton': True,
+ },
+ {
+ 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}',
+ 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'],
+ 'type': 'nsDeviceContextSpecX',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}',
+ 'contract_ids': ['@mozilla.org/gfx/printerlist;1'],
+ 'type': 'nsPrinterListCUPS',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}',
+ 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'],
+ 'type': 'nsPrintSettingsServiceX',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}',
+ 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'],
+ 'type': 'nsPrintDialogServiceX',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{6987230e-0089-4e78-bc5f-1493ee7519fa}',
+ 'contract_ids': ['@mozilla.org/widget/useridleservice;1'],
+ 'type': 'nsUserIdleServiceX',
+ 'singleton': True,
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{84e11f80-ca55-11dd-ad8b-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/system-alerts-service;1'],
+ 'type': 'mozilla::OSXNotificationCenter',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{2451baed-8dc3-46d9-9e30-96e1baa03666}',
+ 'contract_ids': ['@mozilla.org/widget/macdocksupport;1'],
+ 'type': 'nsMacDockSupport',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{74ea4101-a5bb-49bc-9984-66da8b225a37}',
+ 'contract_ids': ['@mozilla.org/widget/macfinderprogress;1'],
+ 'type': 'nsMacFinderProgress',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{de59fe1a-46c8-490f-b04d-34545acb06c9}',
+ 'contract_ids': ['@mozilla.org/widget/macsharingservice;1'],
+ 'type': 'nsMacSharingService',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{29046c8f-cba6-4ffa-9141-1685e96c4ea0}',
+ 'contract_ids': ['@mozilla.org/widget/macuseractivityupdater;1'],
+ 'type': 'nsMacUserActivityUpdater',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{e9096367-ddd9-45e4-b762-49c0c18b7119}',
+ 'contract_ids': ['@mozilla.org/widget/mac-web-app-utils;1'],
+ 'type': 'nsMacWebAppUtils',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{1f39ae50-b6a0-4b37-90f4-60af614193d8}',
+ 'contract_ids': ['@mozilla.org/widget/standalonenativemenu;1'],
+ 'type': 'nsStandaloneNativeMenu',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{b6e1a890-b2b8-4883-a65f-9476f6185313}',
+ 'contract_ids': ['@mozilla.org/widget/systemstatusbar;1'],
+ 'type': 'nsSystemStatusBarCocoa',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{38f396e2-93c9-4a77-aaf7-2d50b9962186}',
+ 'contract_ids': ['@mozilla.org/widget/touchbarupdater;1'],
+ 'type': 'nsTouchBarUpdater',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+]
diff --git a/widget/cocoa/crashtests/373122-1-inner.html b/widget/cocoa/crashtests/373122-1-inner.html
new file mode 100644
index 0000000000..5c14166b75
--- /dev/null
+++ b/widget/cocoa/crashtests/373122-1-inner.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+
+<script>
+function boom()
+{
+ document.body.style.position = "fixed"
+
+ setTimeout(boom2, 1);
+}
+
+function boom2()
+{
+ lappy = document.getElementById("lappy");
+ lappy.style.display = "none"
+
+ setTimeout(boom3, 200);
+}
+
+function boom3()
+{
+ dump("Reloading\n");
+ location.reload();
+}
+
+</script>
+
+
+</head>
+
+
+<body bgcolor="black" onload="boom()">
+
+ <span style="overflow: scroll; display: -moz-box;"></span>
+
+ <embed id="lappy" src="" width=550 height=400 TYPE="application/x-shockwave-flash" ></embed>
+
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/373122-1.html b/widget/cocoa/crashtests/373122-1.html
new file mode 100644
index 0000000000..a57e5f4249
--- /dev/null
+++ b/widget/cocoa/crashtests/373122-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="373122-1-inner.html"></iframe>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/397209-1.html b/widget/cocoa/crashtests/397209-1.html
new file mode 100644
index 0000000000..554b2dac72
--- /dev/null
+++ b/widget/cocoa/crashtests/397209-1.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+</head>
+<body>
+<button style="width: 8205em;"></button>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/403296-1.xhtml b/widget/cocoa/crashtests/403296-1.xhtml
new file mode 100644
index 0000000000..800eaa3558
--- /dev/null
+++ b/widget/cocoa/crashtests/403296-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ class="reftest-wait"
+ style="margin: 12em; padding: 20px 10em; opacity: 0.2; font-size: 11.2px; -moz-appearance: toolbar; white-space: nowrap;"><body
+ style="position: absolute;"
+ onload="setTimeout(function() { document.body.removeChild(document.getElementById('tr')); document.documentElement.removeAttribute('class'); }, 30);">
+
+xxx
+yyy
+
+<tr id="tr">300</tr></body></html>
diff --git a/widget/cocoa/crashtests/419737-1.html b/widget/cocoa/crashtests/419737-1.html
new file mode 100644
index 0000000000..fe6e4532b4
--- /dev/null
+++ b/widget/cocoa/crashtests/419737-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<div><span style="-moz-appearance: radio; padding: 15000px;"></span></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/435223-1.html b/widget/cocoa/crashtests/435223-1.html
new file mode 100644
index 0000000000..c7f70860fc
--- /dev/null
+++ b/widget/cocoa/crashtests/435223-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<div style="min-width: max-content;"><div style="-moz-appearance: button;"><div style="margin: 0 100%;"></div></div></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/444260-1.xhtml b/widget/cocoa/crashtests/444260-1.xhtml
new file mode 100644
index 0000000000..f1a84023df
--- /dev/null
+++ b/widget/cocoa/crashtests/444260-1.xhtml
@@ -0,0 +1,3 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<hbox><button width="7788025414616">S</button></hbox>
+</window>
diff --git a/widget/cocoa/crashtests/444864-1.html b/widget/cocoa/crashtests/444864-1.html
new file mode 100644
index 0000000000..f8bac76e6a
--- /dev/null
+++ b/widget/cocoa/crashtests/444864-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div style="padding: 10px;"><input type="button" value="Go" style="letter-spacing: 331989pt;"></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/449111-1.html b/widget/cocoa/crashtests/449111-1.html
new file mode 100644
index 0000000000..4494591803
--- /dev/null
+++ b/widget/cocoa/crashtests/449111-1.html
@@ -0,0 +1,4 @@
+<html>
+<head></head>
+<body><div style="display: -moz-box; word-spacing: 549755813889px;"><button>T </button></div></body>
+</html>
diff --git a/widget/cocoa/crashtests/460349-1.xhtml b/widget/cocoa/crashtests/460349-1.xhtml
new file mode 100644
index 0000000000..cc9b9700c7
--- /dev/null
+++ b/widget/cocoa/crashtests/460349-1.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head></head>
+<body><div><mstyle xmlns="http://www.w3.org/1998/Math/MathML" style="-moz-appearance: button;"/></div></body>
+</html>
diff --git a/widget/cocoa/crashtests/460387-1.html b/widget/cocoa/crashtests/460387-1.html
new file mode 100644
index 0000000000..cab7e7eb32
--- /dev/null
+++ b/widget/cocoa/crashtests/460387-1.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html><head></head><body><div style="display: table; padding: 625203mm; -moz-appearance: menulist;"></div></body></html>
diff --git a/widget/cocoa/crashtests/464589-1.html b/widget/cocoa/crashtests/464589-1.html
new file mode 100644
index 0000000000..d25d92315d
--- /dev/null
+++ b/widget/cocoa/crashtests/464589-1.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var o2 = document.createElement("option");
+ document.getElementById("o1").appendChild(o2);
+ o2.style.padding = "131072cm";
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<select><option id="o1" style="height: 0cm;"></option></select>
+
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/crashtests.list b/widget/cocoa/crashtests/crashtests.list
new file mode 100644
index 0000000000..0559273a43
--- /dev/null
+++ b/widget/cocoa/crashtests/crashtests.list
@@ -0,0 +1,11 @@
+skip-if(!cocoaWidget) load 373122-1.html # bug 1300017
+load 397209-1.html
+load 403296-1.xhtml
+load 419737-1.html
+load 435223-1.html
+load chrome://reftest/content/crashtests/widget/cocoa/crashtests/444260-1.xhtml
+load 444864-1.html
+load 449111-1.html
+load 460349-1.xhtml
+load 460387-1.html
+load 464589-1.html
diff --git a/widget/cocoa/cursors/arrowN.png b/widget/cocoa/cursors/arrowN.png
new file mode 100644
index 0000000000..c65924091a
--- /dev/null
+++ b/widget/cocoa/cursors/arrowN.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowN@2x.png b/widget/cocoa/cursors/arrowN@2x.png
new file mode 100644
index 0000000000..496f856a1d
--- /dev/null
+++ b/widget/cocoa/cursors/arrowN@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowS.png b/widget/cocoa/cursors/arrowS.png
new file mode 100644
index 0000000000..3975e0837b
--- /dev/null
+++ b/widget/cocoa/cursors/arrowS.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowS@2x.png b/widget/cocoa/cursors/arrowS@2x.png
new file mode 100644
index 0000000000..c7f817afd4
--- /dev/null
+++ b/widget/cocoa/cursors/arrowS@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/cell.png b/widget/cocoa/cursors/cell.png
new file mode 100644
index 0000000000..1400d93f6e
--- /dev/null
+++ b/widget/cocoa/cursors/cell.png
Binary files differ
diff --git a/widget/cocoa/cursors/cell@2x.png b/widget/cocoa/cursors/cell@2x.png
new file mode 100644
index 0000000000..5a1543b16b
--- /dev/null
+++ b/widget/cocoa/cursors/cell@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/colResize.png b/widget/cocoa/cursors/colResize.png
new file mode 100644
index 0000000000..ef4b24936b
--- /dev/null
+++ b/widget/cocoa/cursors/colResize.png
Binary files differ
diff --git a/widget/cocoa/cursors/colResize@2x.png b/widget/cocoa/cursors/colResize@2x.png
new file mode 100644
index 0000000000..d868ee6b57
--- /dev/null
+++ b/widget/cocoa/cursors/colResize@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/help.png b/widget/cocoa/cursors/help.png
new file mode 100644
index 0000000000..7070b4b7bc
--- /dev/null
+++ b/widget/cocoa/cursors/help.png
Binary files differ
diff --git a/widget/cocoa/cursors/help@2x.png b/widget/cocoa/cursors/help@2x.png
new file mode 100644
index 0000000000..7ddf157cd3
--- /dev/null
+++ b/widget/cocoa/cursors/help@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/move.png b/widget/cocoa/cursors/move.png
new file mode 100644
index 0000000000..f6291500a4
--- /dev/null
+++ b/widget/cocoa/cursors/move.png
Binary files differ
diff --git a/widget/cocoa/cursors/move@2x.png b/widget/cocoa/cursors/move@2x.png
new file mode 100644
index 0000000000..d094ce7a15
--- /dev/null
+++ b/widget/cocoa/cursors/move@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/rowResize.png b/widget/cocoa/cursors/rowResize.png
new file mode 100644
index 0000000000..7f68a4007c
--- /dev/null
+++ b/widget/cocoa/cursors/rowResize.png
Binary files differ
diff --git a/widget/cocoa/cursors/rowResize@2x.png b/widget/cocoa/cursors/rowResize@2x.png
new file mode 100644
index 0000000000..e0987a3b95
--- /dev/null
+++ b/widget/cocoa/cursors/rowResize@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNE.png b/widget/cocoa/cursors/sizeNE.png
new file mode 100644
index 0000000000..fd71dea6ab
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNE@2x.png b/widget/cocoa/cursors/sizeNE@2x.png
new file mode 100644
index 0000000000..400f5fe46f
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNESW.png b/widget/cocoa/cursors/sizeNESW.png
new file mode 100644
index 0000000000..5b2c300f4a
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNESW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNESW@2x.png b/widget/cocoa/cursors/sizeNESW@2x.png
new file mode 100644
index 0000000000..7299b98be1
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNESW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNS.png b/widget/cocoa/cursors/sizeNS.png
new file mode 100644
index 0000000000..12b1602f14
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNS.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNS@2x.png b/widget/cocoa/cursors/sizeNS@2x.png
new file mode 100644
index 0000000000..0dc7d15d75
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNS@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNW.png b/widget/cocoa/cursors/sizeNW.png
new file mode 100644
index 0000000000..57d270e0db
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNW@2x.png b/widget/cocoa/cursors/sizeNW@2x.png
new file mode 100644
index 0000000000..312ee61ce4
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNWSE.png b/widget/cocoa/cursors/sizeNWSE.png
new file mode 100644
index 0000000000..d33c2486e4
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNWSE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNWSE@2x.png b/widget/cocoa/cursors/sizeNWSE@2x.png
new file mode 100644
index 0000000000..ecf1438265
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNWSE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSE.png b/widget/cocoa/cursors/sizeSE.png
new file mode 100644
index 0000000000..1689138419
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSE@2x.png b/widget/cocoa/cursors/sizeSE@2x.png
new file mode 100644
index 0000000000..7abce00fd5
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSW.png b/widget/cocoa/cursors/sizeSW.png
new file mode 100644
index 0000000000..5eadafb054
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSW@2x.png b/widget/cocoa/cursors/sizeSW@2x.png
new file mode 100644
index 0000000000..b9ad862aa5
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/vtIBeam.png b/widget/cocoa/cursors/vtIBeam.png
new file mode 100644
index 0000000000..4609922319
--- /dev/null
+++ b/widget/cocoa/cursors/vtIBeam.png
Binary files differ
diff --git a/widget/cocoa/cursors/vtIBeam@2x.png b/widget/cocoa/cursors/vtIBeam@2x.png
new file mode 100644
index 0000000000..a001362b04
--- /dev/null
+++ b/widget/cocoa/cursors/vtIBeam@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomIn.png b/widget/cocoa/cursors/zoomIn.png
new file mode 100644
index 0000000000..7ddfd4056d
--- /dev/null
+++ b/widget/cocoa/cursors/zoomIn.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomIn@2x.png b/widget/cocoa/cursors/zoomIn@2x.png
new file mode 100644
index 0000000000..b1b844fa8c
--- /dev/null
+++ b/widget/cocoa/cursors/zoomIn@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomOut.png b/widget/cocoa/cursors/zoomOut.png
new file mode 100644
index 0000000000..ebacf25889
--- /dev/null
+++ b/widget/cocoa/cursors/zoomOut.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomOut@2x.png b/widget/cocoa/cursors/zoomOut@2x.png
new file mode 100644
index 0000000000..5f84b767ec
--- /dev/null
+++ b/widget/cocoa/cursors/zoomOut@2x.png
Binary files differ
diff --git a/widget/cocoa/docs/index.md b/widget/cocoa/docs/index.md
new file mode 100644
index 0000000000..cda6b2e349
--- /dev/null
+++ b/widget/cocoa/docs/index.md
@@ -0,0 +1,9 @@
+# Firefox on macOS
+
+```{toctree}
+:titlesonly:
+:maxdepth: 1
+:glob:
+
+*
+```
diff --git a/widget/cocoa/docs/macos-apis.md b/widget/cocoa/docs/macos-apis.md
new file mode 100644
index 0000000000..20fa9b6f36
--- /dev/null
+++ b/widget/cocoa/docs/macos-apis.md
@@ -0,0 +1,188 @@
+# Using macOS APIs
+
+With each new macOS release, new APIs are added. Due to the wide range of platforms that Firefox runs on,
+and due to the [wide range of SDKs that we support building with](sdks.md#supported-sdks),
+using macOS APIs in Firefox requires some extra care.
+
+## Availability of APIs, and runtime checks
+
+First of all, if you use an API that is supported by all versions of macOS that Firefox runs on,
+i.e. 10.9 and above, then you don't need to worry about anything:
+The API declaration will be present in any of the supported SDKs, and you don't need any runtime checks.
+
+If you want to use a macOS API that was added after 10.9, then you have to have a runtime check.
+This requirement is completely independent of what SDK is being used for building.
+
+The runtime check [should have the following form](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc#3014609)
+(replace `10.14` with the appropriate version):
+
+```objc++
+if (@available(macOS 10.14, *)) {
+ // Code for macOS 10.14 or later
+} else {
+ // Code for versions earlier than 10.14.
+}
+```
+
+`@available` guards can be used in Objective-C(++) code.
+(In C++ code, you can use [these `nsCocoaFeatures` methods](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsCocoaFeatures.h#21-27) instead.)
+
+For each API, the API declarations in the SDK headers are annotated with `API_AVAILABLE` macros.
+For example, the definition of the `NSVisualEffectMaterial` enum looks like this:
+
+```objc++
+typedef NS_ENUM(NSInteger, NSVisualEffectMaterial) {
+ NSVisualEffectMaterialTitlebar = 3,
+ NSVisualEffectMaterialSelection = 4,
+ NSVisualEffectMaterialMenu API_AVAILABLE(macos(10.11)) = 5,
+ // [...]
+ NSVisualEffectMaterialSheet API_AVAILABLE(macos(10.14)) = 11,
+ // [...]
+} API_AVAILABLE(macos(10.10));
+```
+
+The compiler understands these annotations and makes sure that you wrap all uses of the annotated APIs
+in appropriate `@available` runtime checks.
+
+### Frameworks
+
+In some rare cases, you need functionality from frameworks that are not available on all supported macOS versions.
+Examples of this are `Metal.framework` (added in 10.11) and `MediaPlayer.framework` (added in 10.12.2).
+
+In that case, you can either `dlopen` your framework at runtime ([like we do for MediaPlayer](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/MediaPlayerWrapper.mm#21-27)),
+or you can [use `-weak_framework`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107026)
+[like we do for Metal](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/library/moz.build#301-304):
+
+```python
+if CONFIG['OS_ARCH'] == 'Darwin':
+ OS_LIBS += [
+ # Link to Metal as required by the Metal gfx-hal backend
+ '-weak_framework Metal',
+ ]
+```
+
+## Using new APIs with old SDKs
+
+If you want to use an API that was introduced after 10.12, you now have one extra thing to worry about.
+In addition to the runtime check [described in the previous section](#using-macos-apis), you also
+have to jump through extra hoops in order to allow the build to succeed, because
+[our build target for Firefox has to remain at 10.12 in order for Firefox to run on macOS versions all the way down to macOS 10.12](sdks.md#supported-sdks).
+
+In order to make the compiler accept your code, you will need to copy some amount of the API declaration
+into your own code. Copy it from the newest recent SDK you can get your hands on.
+The exact procedure varies based on the type of API (enum, objc class, method, etc.),
+but the general approach looks like this:
+
+```objc++
+#if !defined(MAC_OS_VERSION_12_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_12_0
+@interface NSScreen (NSScreen12_0)
+// https://developer.apple.com/documentation/appkit/nsscreen/3882821-safeareainsets?language=objc&changes=latest_major
+@property(readonly) NSEdgeInsets safeAreaInsets;
+@end
+#endif
+```
+
+See the [Supporting Multiple SDKs](sdks.md#supporting-multiple-sdks) docs for more information on the `MAC_OS_X_VERSION_MAX_ALLOWED` macro.
+
+Keep these three things in mind:
+
+ - Copy only what you need.
+ - Wrap your declaration in `MAC_OS_X_VERSION_MAX_ALLOWED` checks so that, if an SDK is used that
+ already contains these declarations, your declaration does not conflict with the declaration in the SDK.
+ - Include the `API_AVAILABLE` annotations so that the compiler can protect you from accidentally
+ calling the API on unsupported macOS versions.
+
+Our current code does not always follow the `API_AVAILABLE` advice, but it should.
+
+### Enum types and C structs
+
+If you need a new enum type or C struct, copy the entire type declaration and wrap it in the appropriate ifdefs. Example:
+
+```objc++
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+typedef NS_ENUM(NSUInteger, MPNowPlayingPlaybackState) {
+ MPNowPlayingPlaybackStateUnknown = 0,
+ MPNowPlayingPlaybackStatePlaying,
+ MPNowPlayingPlaybackStatePaused,
+ MPNowPlayingPlaybackStateStopped,
+ MPNowPlayingPlaybackStateInterrupted
+} MP_API(ios(11.0), tvos(11.0), macos(10.12.2), watchos(5.0));
+#endif
+```
+### New enum values for existing enum type
+
+If the enum type itself already exists, but gained a new value, define the value in an unnamed enum:
+
+```objc++
+#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
+enum { NSVisualEffectMaterialSelection = 4 };
+#endif
+```
+
+(This is an example of an interesting case: `NSVisualEffectMaterialSelection` is available starting with
+macOS 10.10, but it's only defined in SDKs starting with the 10.12 SDK.)
+
+### Objective-C classes
+
+For a new Objective-C class, copy the entire `@interface` declaration and wrap it in the appropriate ifdefs.
+
+I haven't personally tested this. If this does not compile (or maybe link?), you can use the following workaround:
+
+ - Define your methods and properties as a category on `NSObject`.
+ - Look up the class at runtime using `NSClassFromString()`.
+ - If you need to create a subclass, do it at runtime using `objc_allocateClassPair` and `class_addMethod`.
+ [Here's an example of that.](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/VibrancyManager.mm#44-60)
+
+### Objective-C properties and methods on an existing class
+
+If an Objective-C class that already exists gains a new method or property, you can "add" it to the
+existing class declaration with the help of a [category](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html):
+
+```objc++
+@interface ExistingClass (YourMadeUpCategoryName)
+// methods and properties here
+@end
+```
+
+### Functions
+
+With free-standing functions I'm not entirely sure what to do.
+In theory, copying the declarations from the new SDK headers should work. Example:
+
+```objc++
+extern "C" {
+ __attribute__((warn_unused_result)) bool
+SecTrustEvaluateWithError(SecTrustRef trust, CFErrorRef _Nullable * _Nullable CF_RETURNS_RETAINED error)
+ API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0));
+
+ __nullable
+CFDataRef SecCertificateCopyNormalizedSubjectSequence(SecCertificateRef certificate)
+ __OSX_AVAILABLE_STARTING(__MAC_10_12_4, __IPHONE_10_3);
+}
+```
+
+I'm not sure what the linker or the dynamic linker do when the symbol is not available.
+Does this require [`__attribute__((weak_import))` annotations](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107262-CJBJAEID)?
+
+And maybe this is where .tbd files in the SDK come in? So that the linker knows which symbols to allow?
+So then that part cannot be worked around by copying code from headers.
+
+Anyway, what always works is the pure runtime approach:
+
+ 1. Define types for the functions you need, but not the functions themselves.
+ 2. At runtime, look up the functions using `dlsym`.
+
+## Notes on Rust
+
+If you call macOS APIs from Rust code, you're kind of on your own. Apple does not provide any Rust
+"headers", so there isn't really an SDK to speak of. So you have to supply your own API declarations
+anyway, regardless of what SDK is being used for building.
+
+In a way, you're side-stepping some of the build time trouble. You don't need to worry about any
+`#ifdefs` because there are no system headers you could conflict with.
+
+On the other hand, you still need to worry about API availability at runtime.
+And in Rust, there are no [availability attributes](https://clang.llvm.org/docs/AttributeReference.html#availability)
+on your API declarations, and there are no
+[`@available` runtime check helpers](https://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available),
+and the compiler cannot warn you if you call APIs outside of availability checks.
diff --git a/widget/cocoa/docs/sdks.md b/widget/cocoa/docs/sdks.md
new file mode 100644
index 0000000000..a7fefa7e51
--- /dev/null
+++ b/widget/cocoa/docs/sdks.md
@@ -0,0 +1,227 @@
+# A primer on macOS SDKs
+
+## Overview
+
+A macOS SDK is an on-disk directory that contains header files and meta information for macOS APIs.
+Apple distributes SDKs as part of the Xcode app bundle. Each Xcode version comes with one macOS SDK,
+the SDK for the most recent released version of macOS at the time of the Xcode release.
+The SDK is located at `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+Compiling Firefox for macOS requires a macOS SDK. The build system bootstraps an adequate SDK by
+default, but you can select a different SDK using the `mozconfig` option `--with-macos-sdk`:
+
+```text
+ac_add_options --with-macos-sdk=/Users/username/SDKs/MacOSX11.3.sdk
+```
+
+## Supported SDKs
+
+First off, Firefox runs on 10.12 and above. This is called the "minimum deployment target" and is
+independent of the SDK version.
+
+Our official Firefox builds compiled in CI (continuous integration) currently use the 13.3 SDK (last updated in [bug 1833998](https://bugzilla.mozilla.org/show_bug.cgi?id=1833998)). This is also the minimum supported SDK version for local builds.
+
+Compiling with different SDKs breaks from time to time. Such breakages should be [reported in Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?blocked=mach-busted&bug_type=defect&cc=:spohl,:mstange&component=General&form_name=enter_bug&keywords=regression&op_sys=macOS&product=Firefox%20Build%20System&rep_platform=All) and fixed quickly.
+
+## Obtaining SDKs
+
+Sometimes you need an SDK that's different from the one in your Xcode.app, for example
+to check whether your code change breaks building with other SDKs, or to verify the
+runtime behavior with the SDK used for CI builds.
+
+The easy but slightly questionable way to obtain an SDK is to download it from a public github repo.
+
+Here's another option:
+
+ 1. Have your Apple ID login details ready, and bring enough time and patience for a 5GB download.
+ 2. Check [these tables in the Xcode wikipedia article](https://en.wikipedia.org/wiki/Xcode#Xcode_7.0_-_10.x_(since_Free_On-Device_Development))
+ and find an Xcode version that contains the SDK you need.
+ 3. Look up the Xcode version number on [xcodereleases.com](https://xcodereleases.com/) and click the Download link for it.
+ 4. Log in with your Apple ID. Then the download should start.
+ 5. Wait for the 5GB Xcode_*.xip download to finish.
+ 6. Open the downloaded xip file. This will extract the Xcode.app bundle.
+ 7. Inside the app bundle, the SDK is at `Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+## Effects of the SDK version
+
+An SDK only contains declarations of APIs. It does not contain the implementations for these APIs.
+
+The implementation of an API is provided by the OS that the app runs on. It is supplied at runtime,
+when your app starts up, by the dynamic linker. For example, the AppKit implementation comes
+from `/System/Library/Frameworks/AppKit.framework` from the OS that the app is run on, regardless
+of what SDK was used when compiling the app.
+
+In other words, building with a macOS SDK of a higher version doesn't magically make new APIs available
+when running on older versions of macOS. And, conversely, building with a lower macOS SDK doesn't limit
+which APIs you can use if your app is run on a newer version of macOS, assuming you manage to convince the
+compiler to accept your code.
+
+The SDK used for building an app determines three things:
+
+ 1. Whether your code compiles at all,
+ 2. which range of macOS versions your app can run on (available deployment targets), and
+ 3. certain aspects of runtime behavior.
+
+The first is straightforward: An SDK contains header files. If you call an API that's not declared
+anywhere - neither in a header file nor in your own code - then your compiler will emit an error.
+(Special case: Calling an unknown Objective-C method usually only emits a warning, not an error.)
+
+The second aspect, available deployment targets, is usually not worth worrying about:
+SDKs have large ranges of supported macOS deployment targets.
+For example, the 10.15 SDK supports running your app on macOS versions all the way back to 10.6.
+This information is written down in the SDK's `SDKSettings.plist`.
+
+The third aspect, varying runtime behavior, is perhaps the most insidious and surprising aspect, and is described
+in the next section.
+
+## Runtime differences based on macOS SDK version
+
+When a new version of macOS is released, existing APIs can change their behavior.
+These changes are usually described in the AppKit release notes:
+
+ - [macOS 10.15 release notes](https://developer.apple.com/documentation/macos_release_notes/macos_catalina_10_15_release_notes?language=objc)
+ - [macOS 10.14 AppKit release notes](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc)
+ - [macOS 10.13 AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/)
+ - [macOS 10.12 and older AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/)
+
+Sometimes, these differences in behavior have the potential to break existing apps. In those instances,
+Apple often provides the old (compatible) behavior until the app is re-built with the new SDK, expecting
+developers to update their apps so that they work with the new behavior, at the same time as
+they update to the new SDK.
+
+Here's an [example from the 10.13 release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling):
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+Here, "linked on or after macOS 10.13" means "linked against the macOS 10.13 SDK or newer".
+
+Apple's expectation is that you upgrade to the new macOS version when it is released, download a new
+Xcode version when it is released, synchronize these updates across the machines of all developers
+that work on your app, use the SDK in the newest Xcode to compile your app, and make changes to your
+app to be compatible with any behavior changes whenever you update Xcode.
+This expectation does not always match reality. It definitely doesn't match what we're doing with Firefox.
+
+For Firefox, SDK-dependent compatibility behaviors mean that developers who build Firefox locally
+can see different runtime behavior than the users of our CI builds, if they use a different SDK than
+the SDK used in CI.
+That is, unless we change the Firefox code so that it has the same behavior regardless of SDK version.
+Often this can be achieved by using APIs in a way that's more in line with the API's recommended use.
+
+For example, we've had cases of
+[broken placeholder text in search fields](https://bugzilla.mozilla.org/show_bug.cgi?id=1273106),
+[missing](https://bugzilla.mozilla.org/show_bug.cgi?id=941325) or [double-drawn focus rings](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsNativeThemeCocoa.mm#149-169),
+[a startup crash](https://bugzilla.mozilla.org/show_bug.cgi?id=1516437),
+[fully black windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1494022),
+[fully gray windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1576113#c4),
+[broken vibrancy](https://bugzilla.mozilla.org/show_bug.cgi?id=1475694), and
+[broken colors in dark mode](https://bugzilla.mozilla.org/show_bug.cgi?id=1578917).
+
+In most of these cases, the breakage was either very minor, or it was caused by Firefox doing things
+that were explicitly discouraged, like creating unexpected NSView hierarchies, or relying on unspecified
+implementation details. (With one exception: In 10.14, HiDPI-aware `NSOpenGLContext` rendering in
+layer-backed windows simply broke.)
+
+And in all of these cases, it was the SDK-dependent compatibility behavior that protected our users from being
+exposed to the breakage. Our CI builds continued to work because they were built with an older SDK.
+
+We have addressed all known cases of breakage when building Firefox with newer SDKs.
+I am not aware of any current instances of this problem as of this writing (June 2020).
+
+For more information about how these compatibility tricks work,
+read the [Overriding SDK-dependent runtime behavior](#overriding-sdk-dependent-runtime-behavior) section.
+
+## Supporting multiple SDKs
+
+As described under [Supported SDKs](#supported-sdks), Firefox can be built with a wide variety of SDK versions.
+
+This ability comes at the cost of some manual labor; it requires some well-placed `#ifdefs` and
+copying of header definitions.
+
+Every SDK defines the macro `MAC_OS_X_VERSION_MAX_ALLOWED` with a value that matches the SDK version,
+in the SDK's `AvailabilityMacros.h` header. This header also defines version constants like `MAC_OS_X_VERSION_10_12`.
+For example, I have a version of the 10.12 SDK which contains the line
+
+```cpp
+#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_12_4
+```
+
+The name `MAC_OS_X_VERSION_MAX_ALLOWED` is rather misleading; a better name would be
+`MAC_OS_X_VERSION_MAX_KNOWN_BY_SDK`. Compiling with an old SDK *does not* prevent apps from running
+on newer versions of macOS.
+
+With the help of the `MAC_OS_X_VERSION_MAX_ALLOWED` macro, we can make our code adapt to the SDK that's
+being used. Here's [an example](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/xre/MacApplicationDelegate.mm#345-351) where the 10.14 SDK changed the signature of
+[an `NSApplicationDelegate` method](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428471-application?language=objc):
+
+```objc++
+- (BOOL)application:(NSApplication*)application
+ continueUserActivity:(NSUserActivity*)userActivity
+#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14
+ restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler {
+#else
+ restorationHandler:(void (^)(NSArray*))restorationHandler {
+#endif
+ ...
+}
+```
+
+We can also use this macro to supply missing API definitions in such a way that
+they don't conflict with the definitions from the SDK.
+This is described in the "Using macOS APIs" document, under [Using new APIs with old SDKs](./macos-apis.md#using-new-apis-with-old-sdks).
+
+## Overriding SDK-dependent runtime behavior
+
+This section contains some more details on the compatibility tricks that cause different runtime
+behavior dependent on the SDK, as described in
+[Runtime differences based on macOS SDK version](#runtime-differences-based-on-macos-sdk-version).
+
+### How it works
+
+AppKit is the one system framework I know of that employs these tricks. Let's explore how AppKit makes this work,
+by going back to the [NSCollectionView example](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling) from above:
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+For each of these SDK-dependent behavior differences, both the old and the new behavior are implemented
+in the version of AppKit that ships with the new macOS version.
+At runtime, AppKit selects one of the behaviors based on the SDK version, with a call to
+`_CFExecutableLinkedOnOrAfter()`. This call checks the SDK version of the main executable of the
+process that's running AppKit code; in our case that's the `firefox` or `plugin-container` executable.
+The SDK version is stored in the mach-o headers of the executable by the linker.
+
+One interesting design aspect of AppKit's compatibility tricks is the fact that most of these behavior differences
+can be toggled with a "user default" preference.
+For example, the "responsive scrolling in NSCollectionViews" behavior change can be controlled with
+a user default with the name "NSCollectionViewPrefetchingEnabled".
+The SDK check only happens if "NSCollectionViewPrefetchingEnabled" is not set to either YES or NO.
+
+More precisely, this example works as follows:
+
+ - `-[NSCollectionView prepareContentInRect:]` is the function that supports both the old and the new behavior.
+ - It calls `_NSGetBoolAppConfig` for the value "NSCollectionViewPrefetchingEnabled", and also supplies a "default
+ value function".
+ - If the user default is not set, the default value function is called. This function has the name
+ `NSCollectionViewPrefetchingEnabledDefaultValueFunction`.
+ - `NSCollectionViewPrefetchingEnabledDefaultValueFunction` calls `_CFExecutableLinkedOnOrAfter(13)`.
+
+You can find many similar toggles if you list the AppKit symbols that end in `DefaultValueFunction`,
+for example by executing `nm /System/Library/Frameworks/AppKit.framework/AppKit | grep DefaultValueFunction`.
+
+### Overriding SDK-dependent runtime behavior
+
+You can set these preferences programmatically, in a way that `_NSGetBoolAppConfig()` can pick them up,
+for example with [`registerDefaults`](https://developer.apple.com/documentation/foundation/nsuserdefaults/1417065-registerdefaults?language=objc)
+or like this:
+
+```objc++
+[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSViewAllowsRootLayerBacking"];
+```
+
+The AppKit release notes mention this ability but ask for it to only be used for debugging purposes:
+
+> In some cases, we provide defaults (preferences) settings which can be used to get the old or new behavior,
+> independent of what system an application was built against. Often these preferences are provided for
+> debugging purposes only; in some cases the preferences can be used to globally modify the behavior
+> of an application by registering the values (do it somewhere very early, with `-[NSUserDefaults registerDefaults:]`).
+
+It's interesting that they mention this at all because, as far as I can tell, none of these values are documented.
diff --git a/widget/cocoa/metrics.yaml b/widget/cocoa/metrics.yaml
new file mode 100644
index 0000000000..d26bfd21d3
--- /dev/null
+++ b/widget/cocoa/metrics.yaml
@@ -0,0 +1,28 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Widget: Cocoa'
+
+startup:
+ is_restored_by_macos:
+ type: boolean
+ description: >
+ Recorded on every launch of a Firefox install on macOS, with a boolean
+ value indicating whether Firefox was restored by macOS or if it was
+ manually launched by a user.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=639707
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=639707
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - spohl@mozilla.com
+ expires: 116
diff --git a/widget/cocoa/moz.build b/widget/cocoa/moz.build
new file mode 100644
index 0000000000..c996e62ad6
--- /dev/null
+++ b/widget/cocoa/moz.build
@@ -0,0 +1,182 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+ SCHEDULES.exclusive = ["macosx"]
+
+with Files("*TextInput*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+XPIDL_SOURCES += [
+ "nsPIWidgetCocoa.idl",
+]
+
+XPIDL_MODULE = "widget_cocoa"
+
+EXPORTS += [
+ "CFTypeRefPtr.h",
+ "DesktopBackgroundImage.h",
+ "MediaHardwareKeysEventSourceMac.h",
+ "MediaHardwareKeysEventSourceMacMediaCenter.h",
+ "mozView.h",
+ "nsBidiKeyboard.h",
+ "nsChangeObserver.h",
+ "nsCocoaFeatures.h",
+ "nsCocoaUtils.h",
+ "SDKDeclarations.h",
+]
+
+UNIFIED_SOURCES += [
+ "AppearanceOverride.mm",
+ "GfxInfo.mm",
+ "MOZIconHelper.mm",
+ "MOZMenuOpeningCoordinator.mm",
+ "NativeKeyBindings.mm",
+ "NativeMenuMac.mm",
+ "NativeMenuSupport.mm",
+ "nsAppShell.mm",
+ "nsBidiKeyboard.mm",
+ "nsCocoaFeatures.mm",
+ "nsCocoaUtils.mm",
+ "nsCocoaWindow.mm",
+ "nsColorPicker.mm",
+ "nsCursorManager.mm",
+ "nsDeviceContextSpecX.mm",
+ "nsFilePicker.mm",
+ "nsLookAndFeel.mm",
+ "nsMacCursor.mm",
+ "nsMacDockSupport.mm",
+ "nsMacFinderProgress.mm",
+ "nsMacSharingService.mm",
+ "nsMacUserActivityUpdater.mm",
+ "nsMacWebAppUtils.mm",
+ "nsMenuBarX.mm",
+ "nsMenuGroupOwnerX.mm",
+ "nsMenuItemIconX.mm",
+ "nsMenuItemX.mm",
+ "nsMenuUtilsX.mm",
+ "nsMenuX.mm",
+ "nsPrintDialogX.mm",
+ "nsPrintSettingsServiceX.mm",
+ "nsPrintSettingsX.mm",
+ "nsSound.mm",
+ "nsStandaloneNativeMenu.mm",
+ "nsSystemStatusBarCocoa.mm",
+ "nsToolkit.mm",
+ "nsTouchBar.mm",
+ "nsTouchBarInput.mm",
+ "nsTouchBarInputIcon.mm",
+ "nsTouchBarUpdater.mm",
+ "nsUserIdleServiceX.mm",
+ "nsWidgetFactory.mm",
+ "nsWindowMap.mm",
+ "OSXNotificationCenter.mm",
+ "ScreenHelperCocoa.mm",
+ "TextInputHandler.mm",
+ "TextRecognition.mm",
+ "VibrancyManager.mm",
+ "ViewRegion.mm",
+ "WidgetTraceEvent.mm",
+]
+
+# These files cannot be built in unified mode because they cause symbol conflicts
+SOURCES += [
+ "DesktopBackgroundImage.mm",
+ "MediaHardwareKeysEventSourceMac.mm",
+ "MediaHardwareKeysEventSourceMacMediaCenter.mm",
+ "MediaKeysEventSourceFactory.cpp",
+ "nsChildView.mm",
+ "nsClipboard.mm",
+ "nsDragService.mm",
+ "nsNativeThemeCocoa.mm",
+]
+
+if not CONFIG["RELEASE_OR_BETA"] or CONFIG["MOZ_DEBUG"]:
+ SOURCES += [
+ "nsSandboxViolationSink.mm",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/dom/events",
+ "/dom/media/platforms/apple",
+ "/layout/base",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/xul",
+ "/widget",
+ "/widget/headless",
+]
+
+if CONFIG["MOZ_ENABLE_SKIA_PDF"]:
+ # Skia includes because widget code includes PrintTargetSkPDF.h, and that
+ # includes skia headers.
+ LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"]
+
+RESOURCE_FILES.cursors += [
+ "cursors/arrowN.png",
+ "cursors/arrowN@2x.png",
+ "cursors/arrowS.png",
+ "cursors/arrowS@2x.png",
+ "cursors/cell.png",
+ "cursors/cell@2x.png",
+ "cursors/colResize.png",
+ "cursors/colResize@2x.png",
+ "cursors/help.png",
+ "cursors/help@2x.png",
+ "cursors/move.png",
+ "cursors/move@2x.png",
+ "cursors/rowResize.png",
+ "cursors/rowResize@2x.png",
+ "cursors/sizeNE.png",
+ "cursors/sizeNE@2x.png",
+ "cursors/sizeNESW.png",
+ "cursors/sizeNESW@2x.png",
+ "cursors/sizeNS.png",
+ "cursors/sizeNS@2x.png",
+ "cursors/sizeNW.png",
+ "cursors/sizeNW@2x.png",
+ "cursors/sizeNWSE.png",
+ "cursors/sizeNWSE@2x.png",
+ "cursors/sizeSE.png",
+ "cursors/sizeSE@2x.png",
+ "cursors/sizeSW.png",
+ "cursors/sizeSW@2x.png",
+ "cursors/vtIBeam.png",
+ "cursors/vtIBeam@2x.png",
+ "cursors/zoomIn.png",
+ "cursors/zoomIn@2x.png",
+ "cursors/zoomOut.png",
+ "cursors/zoomOut@2x.png",
+]
+
+# These resources go in $(DIST)/bin/res/MainMenu.nib, but we can't use a magic
+# RESOURCE_FILES.MainMenu.nib attribute, since that would put the files in
+# $(DIST)/bin/res/MainMenu/nib. Instead, we call __setattr__ directly to create
+# an attribute with the correct name.
+RESOURCE_FILES.__setattr__(
+ "MainMenu.nib",
+ [
+ "resources/MainMenu.nib/classes.nib",
+ "resources/MainMenu.nib/info.nib",
+ "resources/MainMenu.nib/keyedobjects.nib",
+ ],
+)
+
+OS_LIBS += [
+ "-framework IOSurface",
+ "-framework Vision",
+]
+
+SPHINX_TREES["/widget/cocoa"] = "docs"
diff --git a/widget/cocoa/mozView.h b/widget/cocoa/mozView.h
new file mode 100644
index 0000000000..deb719254e
--- /dev/null
+++ b/widget/cocoa/mozView.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef mozView_h_
+#define mozView_h_
+
+#undef DARWIN
+#import <Cocoa/Cocoa.h>
+class nsIWidget;
+
+namespace mozilla {
+namespace widget {
+class TextInputHandler;
+} // namespace widget
+} // namespace mozilla
+
+// A protocol with some of the methods that ChildView implements. In the distant
+// past, this protocol was used by embedders: They would create their own NSView
+// subclass, implement mozView on it, and then embed a Gecko ChildView by adding
+// it as a subview of this view. This scenario no longer exists.
+// Now this protocol is mostly just used by TextInputHandler and mozAccessible
+// in order to communicate with ChildView without seeing the entire ChildView
+// interface definition.
+@protocol mozView
+
+// aHandler is Gecko's default text input handler: It implements the
+// NSTextInput protocol to handle key events. Don't make aHandler a
+// strong reference -- that causes a memory leak.
+- (void)installTextInputHandler:(mozilla::widget::TextInputHandler*)aHandler;
+- (void)uninstallTextInputHandler;
+
+// access the nsIWidget associated with this view. DOES NOT ADDREF.
+- (nsIWidget*)widget;
+
+// called when our corresponding Gecko view goes away
+- (void)widgetDestroyed;
+
+- (BOOL)isDragInProgress;
+
+// Checks whether the view is first responder or not
+- (BOOL)isFirstResponder;
+
+// Call when you dispatch an event which may cause to open context menu.
+- (void)maybeInitContextMenuTracking;
+
+@end
+
+// An informal protocol implemented by the NSWindow of the host application.
+//
+// It's used to prevent re-entrant calls to -makeKeyAndOrderFront: when gecko
+// focus/activate events propagate out to the embedder's
+// nsIEmbeddingSiteWindow::SetFocus implementation.
+@interface NSObject (mozWindow)
+
+- (BOOL)suppressMakeKeyFront;
+- (void)setSuppressMakeKeyFront:(BOOL)inSuppress;
+
+@end
+
+#endif // mozView_h_
diff --git a/widget/cocoa/nsAppShell.h b/widget/cocoa/nsAppShell.h
new file mode 100644
index 0000000000..009768794f
--- /dev/null
+++ b/widget/cocoa/nsAppShell.h
@@ -0,0 +1,92 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+/*
+ * Runs the main native Cocoa run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#ifndef nsAppShell_h_
+#define nsAppShell_h_
+
+#import <AppKit/NSApplication.h>
+
+#include "nsBaseAppShell.h"
+#include "nsTArray.h"
+
+class ProfilingStack;
+
+namespace mozilla {
+class nsAvailableMemoryWatcherBase;
+}
+
+// GeckoNSApplication
+//
+// Subclass of NSApplication for filtering out certain events.
+@interface GeckoNSApplication : NSApplication {
+}
+@end
+
+@class AppShellDelegate;
+
+class nsAppShell : public nsBaseAppShell {
+ public:
+ NS_IMETHOD ResumeNative(void) override;
+
+ nsAppShell();
+
+ nsresult Init();
+
+ NS_IMETHOD Run(void) override;
+ NS_IMETHOD Exit(void) override;
+ NS_IMETHOD OnProcessNextEvent(nsIThreadInternal* aThread, bool aMayWait) override;
+ NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal* aThread, bool aEventWasProcessed) override;
+
+ void OnRunLoopActivityChanged(CFRunLoopActivity aActivity);
+
+ // public only to be visible to Objective-C code that must call it
+ void WillTerminate();
+
+ static void OnMemoryPressureChanged(dispatch_source_memorypressure_flags_t aPressureLevel);
+
+ protected:
+ virtual ~nsAppShell();
+
+ virtual void ScheduleNativeEventCallback() override;
+ virtual bool ProcessNextNativeEvent(bool aMayWait) override;
+
+ void InitMemoryPressureObserver();
+
+ static void ProcessGeckoEvents(void* aInfo);
+
+ protected:
+ CFMutableArrayRef mAutoreleasePools;
+
+ AppShellDelegate* mDelegate;
+ CFRunLoopRef mCFRunLoop;
+ CFRunLoopSourceRef mCFRunLoopSource;
+
+ // An observer for the profiler that is notified when the event loop enters
+ // and exits the waiting state.
+ CFRunLoopObserverRef mCFRunLoopObserver;
+
+ // Non-null while the native event loop is in the waiting state.
+ ProfilingStack* mProfilingStackWhileWaiting = nullptr;
+
+ // For getting notifications from the OS about memory pressure state changes.
+ dispatch_source_t mMemoryPressureSource = nullptr;
+
+ bool mRunningEventLoop;
+ bool mStarted;
+ bool mTerminated;
+ bool mSkippedNativeCallback;
+ bool mRunningCocoaEmbedded;
+
+ int32_t mNativeEventCallbackDepth;
+ // Can be set from different threads, so must be modified atomically
+ int32_t mNativeEventScheduledDepth;
+};
+
+#endif // nsAppShell_h_
diff --git a/widget/cocoa/nsAppShell.mm b/widget/cocoa/nsAppShell.mm
new file mode 100644
index 0000000000..9ba512ce49
--- /dev/null
+++ b/widget/cocoa/nsAppShell.mm
@@ -0,0 +1,1120 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/*
+ * Runs the main native Cocoa run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+#include <dlfcn.h>
+
+#include "mozilla/AvailableMemoryWatcher.h"
+#include "CustomCocoaEvents.h"
+#include "mozilla/WidgetTraceEvent.h"
+#include "nsAppShell.h"
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsMemoryPressure.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaFeatures.h"
+#include "nsChildView.h"
+#include "nsToolkit.h"
+#include "TextInputHandler.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "ScreenHelperCocoa.h"
+#include "mozilla/Hal.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerThreadSleep.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "HeadlessScreenHelper.h"
+#include "MOZMenuOpeningCoordinator.h"
+#include "pratom.h"
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+# include "nsSandboxViolationSink.h"
+#endif
+
+#include <IOKit/pwr_mgt/IOPMLib.h>
+#include "nsIDOMWakeLockListener.h"
+#include "nsIPowerManagerService.h"
+
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#define WAKE_LOCK_LOG(...) MOZ_LOG(gMacWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule gMacWakeLockLog("MacWakeLock");
+
+// A wake lock listener that disables screen saver when requested by
+// Gecko. For example when we're playing video in a foreground tab we
+// don't want the screen saver to turn on.
+
+class MacWakeLockListener final : public nsIDOMMozWakeLockListener {
+ public:
+ NS_DECL_ISUPPORTS;
+
+ private:
+ ~MacWakeLockListener() {}
+
+ IOPMAssertionID mAssertionNoDisplaySleepID = kIOPMNullAssertionID;
+ IOPMAssertionID mAssertionNoIdleSleepID = kIOPMNullAssertionID;
+
+ NS_IMETHOD Callback(const nsAString& aTopic, const nsAString& aState) override {
+ if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") &&
+ !aTopic.EqualsASCII("video-playing")) {
+ return NS_OK;
+ }
+
+ // we should still hold the lock for background audio.
+ if (aTopic.EqualsASCII("audio-playing") && aState.EqualsASCII("locked-background")) {
+ WAKE_LOCK_LOG("keep audio playing even in background");
+ return NS_OK;
+ }
+
+ bool shouldKeepDisplayOn = aTopic.EqualsASCII("screen") || aTopic.EqualsASCII("video-playing");
+ CFStringRef assertionType =
+ shouldKeepDisplayOn ? kIOPMAssertionTypeNoDisplaySleep : kIOPMAssertionTypeNoIdleSleep;
+ IOPMAssertionID& assertionId =
+ shouldKeepDisplayOn ? mAssertionNoDisplaySleepID : mAssertionNoIdleSleepID;
+ WAKE_LOCK_LOG("topic=%s, state=%s, shouldKeepDisplayOn=%d", NS_ConvertUTF16toUTF8(aTopic).get(),
+ NS_ConvertUTF16toUTF8(aState).get(), shouldKeepDisplayOn);
+
+ // Note the wake lock code ensures that we're not sent duplicate
+ // "locked-foreground" notifications when multiple wake locks are held.
+ if (aState.EqualsASCII("locked-foreground")) {
+ if (assertionId != kIOPMNullAssertionID) {
+ WAKE_LOCK_LOG("already has a lock");
+ return NS_OK;
+ }
+ // Prevent screen saver.
+ CFStringRef cf_topic = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aTopic.Data()), aTopic.Length());
+ IOReturn success = ::IOPMAssertionCreateWithName(assertionType, kIOPMAssertionLevelOn,
+ cf_topic, &assertionId);
+ CFRelease(cf_topic);
+ if (success != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to disable screensaver");
+ }
+ WAKE_LOCK_LOG("create screensaver");
+ } else {
+ // Re-enable screen saver.
+ if (assertionId != kIOPMNullAssertionID) {
+ IOReturn result = ::IOPMAssertionRelease(assertionId);
+ if (result != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to release screensaver");
+ }
+ WAKE_LOCK_LOG("Release screensaver");
+ assertionId = kIOPMNullAssertionID;
+ }
+ }
+ return NS_OK;
+ }
+}; // MacWakeLockListener
+
+// defined in nsCocoaWindow.mm
+extern int32_t gXULModalLevel;
+
+static bool gAppShellMethodsSwizzled = false;
+
+void OnUncaughtException(NSException* aException) {
+ nsObjCExceptionLog(aException);
+ MOZ_CRASH("Uncaught Objective C exception from NSSetUncaughtExceptionHandler");
+}
+
+@implementation GeckoNSApplication
+
+// Load is called very early during startup, when the Objective C runtime loads this class.
++ (void)load {
+ NSSetUncaughtExceptionHandler(OnUncaughtException);
+}
+
+// This method is called from NSDefaultTopLevelErrorHandler, which is invoked when an Objective C
+// exception propagates up into the native event loop. It is possible that it is also called in
+// other cases.
+- (void)reportException:(NSException*)aException {
+ if (ShouldIgnoreObjCException(aException)) {
+ return;
+ }
+
+ nsObjCExceptionLog(aException);
+
+#ifdef NIGHTLY_BUILD
+ MOZ_CRASH("Uncaught Objective C exception from -[GeckoNSApplication reportException:]");
+#endif
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+
+ if ([anEvent type] == NSEventTypeApplicationDefined && [anEvent subtype] == kEventSubtypeTrace) {
+ mozilla::SignalTracerThread();
+ return;
+ }
+ [super sendEvent:anEvent];
+}
+
+- (NSEvent*)nextEventMatchingMask:(NSEventMask)mask
+ untilDate:(NSDate*)expiration
+ inMode:(NSString*)mode
+ dequeue:(BOOL)flag {
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+ NSEvent* nextEvent = [super nextEventMatchingMask:mask
+ untilDate:expiration
+ inMode:mode
+ dequeue:flag];
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ }
+ return nextEvent;
+}
+
+@end
+
+// AppShellDelegate
+//
+// Cocoa bridge class. An object of this class is registered to receive
+// notifications.
+//
+@interface AppShellDelegate : NSObject {
+ @private
+ nsAppShell* mAppShell;
+}
+
+- (id)initWithAppShell:(nsAppShell*)aAppShell;
+- (void)applicationWillTerminate:(NSNotification*)aNotification;
+- (BOOL)shouldSaveApplicationState:(NSCoder*)coder;
+- (BOOL)shouldRestoreApplicationState:(NSCoder*)coder;
+@end
+
+// nsAppShell implementation
+
+NS_IMETHODIMP
+nsAppShell::ResumeNative(void) {
+ nsresult retval = nsBaseAppShell::ResumeNative();
+ if (NS_SUCCEEDED(retval) && (mSuspendNativeCount == 0) && mSkippedNativeCallback) {
+ mSkippedNativeCallback = false;
+ ScheduleNativeEventCallback();
+ }
+ return retval;
+}
+
+nsAppShell::nsAppShell()
+ : mAutoreleasePools(nullptr),
+ mDelegate(nullptr),
+ mCFRunLoop(NULL),
+ mCFRunLoopSource(NULL),
+ mRunningEventLoop(false),
+ mStarted(false),
+ mTerminated(false),
+ mSkippedNativeCallback(false),
+ mNativeEventCallbackDepth(0),
+ mNativeEventScheduledDepth(0) {
+ // A Cocoa event loop is running here if (and only if) we've been embedded
+ // by a Cocoa app.
+ mRunningCocoaEmbedded = [NSApp isRunning] ? true : false;
+}
+
+nsAppShell::~nsAppShell() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ hal::Shutdown();
+
+ if (mMemoryPressureSource) {
+ dispatch_release(mMemoryPressureSource);
+ mMemoryPressureSource = nullptr;
+ }
+
+ if (mCFRunLoop) {
+ if (mCFRunLoopSource) {
+ ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopSource);
+ }
+ if (mCFRunLoopObserver) {
+ ::CFRunLoopRemoveObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopObserver);
+ }
+ ::CFRelease(mCFRunLoop);
+ }
+
+ if (mAutoreleasePools) {
+ NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0,
+ "nsAppShell destroyed without popping all autorelease pools");
+ ::CFRelease(mAutoreleasePools);
+ }
+
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+NS_IMPL_ISUPPORTS(MacWakeLockListener, nsIDOMMozWakeLockListener)
+mozilla::StaticRefPtr<MacWakeLockListener> sWakeLockListener;
+
+static void AddScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sWakeLockListener = new MacWakeLockListener();
+ sPowerManagerService->AddWakeLockListener(sWakeLockListener);
+ } else {
+ NS_WARNING("Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+static void RemoveScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sPowerManagerService->RemoveWakeLockListener(sWakeLockListener);
+ sPowerManagerService = nullptr;
+ sWakeLockListener = nullptr;
+ }
+}
+
+void RunLoopObserverCallback(CFRunLoopObserverRef aObserver, CFRunLoopActivity aActivity,
+ void* aInfo) {
+ static_cast<nsAppShell*>(aInfo)->OnRunLoopActivityChanged(aActivity);
+}
+
+void nsAppShell::OnRunLoopActivityChanged(CFRunLoopActivity aActivity) {
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // When the event loop is in its waiting state, we would like the profiler to know that the thread
+ // is idle. The usual way to notify the profiler of idleness would be to place a profiler label
+ // frame with the IDLE category on the stack, for the duration of the function that does the
+ // waiting. However, since macOS uses an event loop model where "the event loop calls you", we do
+ // not control the function that does the waiting; the waiting happens inside CFRunLoop code.
+ // Instead, the run loop notifies us when it enters and exits the waiting state, by calling this
+ // function.
+ // So we do not have a function under our control that stays on the stack for the duration of the
+ // wait. So, rather than putting an AutoProfilerLabel on the stack, we will manually push and pop
+ // the label frame here.
+ // The location in the stack where this label frame is inserted is somewhat arbitrary. In
+ // practice, the label frame will be at the very tip of the stack, looking like it's "inside" the
+ // mach_msg_trap wait function.
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ using ThreadRegistration = mozilla::profiler::ThreadRegistration;
+ ThreadRegistration::WithOnThreadRef([&](ThreadRegistration::OnThreadRef aOnThreadRef) {
+ ProfilingStack& profilingStack =
+ aOnThreadRef.UnlockedConstReaderAndAtomicRWRef().ProfilingStackRef();
+ mProfilingStackWhileWaiting = &profilingStack;
+ uint8_t variableOnStack = 0;
+ profilingStack.pushLabelFrame("Native event loop idle", nullptr, &variableOnStack,
+ JS::ProfilingCategoryPair::IDLE, 0);
+ profiler_thread_sleep();
+ });
+ } else {
+ if (mProfilingStackWhileWaiting) {
+ mProfilingStackWhileWaiting->pop();
+ mProfilingStackWhileWaiting = nullptr;
+ profiler_thread_wake();
+ }
+ }
+}
+
+// Init
+//
+// Loads the nib (see bug 316076c21) and sets up the CFRunLoopSource used to
+// interrupt the main native run loop.
+//
+// public
+nsresult nsAppShell::Init() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // No event loop is running yet (unless an embedding app that uses
+ // NSApplicationMain() is running).
+ NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init];
+
+ char* mozAppNoDock = PR_GetEnv("MOZ_APP_NO_DOCK");
+ if (mozAppNoDock && strcmp(mozAppNoDock, "") != 0) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
+ }
+
+ // mAutoreleasePools is used as a stack of NSAutoreleasePool objects created
+ // by |this|. CFArray is used instead of NSArray because NSArray wants to
+ // retain each object you add to it, and you can't retain an
+ // NSAutoreleasePool.
+ mAutoreleasePools = ::CFArrayCreateMutable(nullptr, 0, nullptr);
+ NS_ENSURE_STATE(mAutoreleasePools);
+
+ bool isNSApplicationProcessType = (XRE_GetProcessType() != GeckoProcessType_RDD) &&
+ (XRE_GetProcessType() != GeckoProcessType_Socket);
+
+ if (isNSApplicationProcessType) {
+ // This call initializes NSApplication unless:
+ // 1) we're using xre -- NSApp's already been initialized by
+ // MacApplicationDelegate.mm's EnsureUseCocoaDockAPI().
+ // 2) an embedding app that uses NSApplicationMain() is running -- NSApp's
+ // already been initialized and its main run loop is already running.
+ [[NSBundle mainBundle] loadNibNamed:@"res/MainMenu"
+ owner:[GeckoNSApplication sharedApplication]
+ topLevelObjects:nil];
+ }
+
+ mDelegate = [[AppShellDelegate alloc] initWithAppShell:this];
+ NS_ENSURE_STATE(mDelegate);
+
+ // Add a CFRunLoopSource to the main native run loop. The source is
+ // responsible for interrupting the run loop when Gecko events are ready.
+
+ mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
+ NS_ENSURE_STATE(mCFRunLoop);
+ ::CFRetain(mCFRunLoop);
+
+ CFRunLoopSourceContext context;
+ bzero(&context, sizeof(context));
+ // context.version = 0;
+ context.info = this;
+ context.perform = ProcessGeckoEvents;
+
+ mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
+ NS_ENSURE_STATE(mCFRunLoopSource);
+
+ ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+
+ // Add a CFRunLoopObserver so that the profiler can be notified when we enter and exit the waiting
+ // state.
+ CFRunLoopObserverContext observerContext;
+ PodZero(&observerContext);
+ observerContext.info = this;
+
+ mCFRunLoopObserver = ::CFRunLoopObserverCreate(
+ kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting | kCFRunLoopExit, true,
+ 0, RunLoopObserverCallback, &observerContext);
+ NS_ENSURE_STATE(mCFRunLoopObserver);
+
+ ::CFRunLoopAddObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes);
+
+ hal::Init();
+
+ if (XRE_IsParentProcess()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+
+ if (gfxPlatform::IsHeadless()) {
+ screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+ } else {
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperCocoa>());
+ }
+
+ InitMemoryPressureObserver();
+ }
+
+ nsresult rv = nsBaseAppShell::Init();
+
+ if (isNSApplicationProcessType && !gAppShellMethodsSwizzled) {
+ // We should only replace the original terminate: method if we're not
+ // running in a Cocoa embedder. See bug 604901.
+ if (!mRunningCocoaEmbedded) {
+ nsToolkit::SwizzleMethods([NSApplication class], @selector(terminate:),
+ @selector(nsAppShell_NSApplication_terminate:));
+ }
+ gAppShellMethodsSwizzled = true;
+ }
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ if (Preferences::GetBool("security.sandbox.mac.track.violations", false)) {
+ nsSandboxViolationSink::Start();
+ }
+#endif
+
+ [localPool release];
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// ProcessGeckoEvents
+//
+// The "perform" target of mCFRunLoop, called when mCFRunLoopSource is
+// signalled from ScheduleNativeEventCallback.
+//
+// Arrange for Gecko events to be processed on demand (in response to a call
+// to ScheduleNativeEventCallback(), if processing of Gecko events via "native
+// methods" hasn't been suspended). This happens in NativeEventCallback().
+//
+// protected static
+void nsAppShell::ProcessGeckoEvents(void* aInfo) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessGeckoEvents", OTHER);
+
+ nsAppShell* self = static_cast<nsAppShell*>(aInfo);
+
+ if (self->mRunningEventLoop) {
+ self->mRunningEventLoop = false;
+
+ // The run loop may be sleeping -- [NSRunLoop runMode:...]
+ // won't return until it's given a reason to wake up. Awaken it by
+ // posting a bogus event. There's no need to make the event
+ // presentable.
+ //
+ // But _don't_ set windowNumber to '-1' -- that can lead to nasty
+ // weirdness like bmo bug 397039 (a crash in [NSApp sendEvent:] on one of
+ // these fake events, because the -1 has gotten changed into the number
+ // of an actual NSWindow object, and that NSWindow object has just been
+ // destroyed). Setting windowNumber to '0' seems to work fine -- this
+ // seems to prevent the OS from ever trying to associate our bogus event
+ // with a particular NSWindow object.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ // Previously we used to send this second event regardless of
+ // self->mRunningEventLoop. However, that was removed in bug 1690687 for
+ // performance reasons. It is still needed for the mRunningEventLoop case
+ // otherwise we'll get in a cycle of sending postEvent followed by the
+ // DummyEvent inserted by nsBaseAppShell::OnProcessNextEvent. This second
+ // event will cause the second call to AcquireFirstMatchingEventInQueue in
+ // nsAppShell::ProcessNextNativeEvent to return true. Which makes
+ // nsBaseAppShell::OnProcessNextEvent call nsAppShell::ProcessNextNativeEvent
+ // again during which it will loop until it sleeps because ProcessGeckoEvents()
+ // won't be called for the DummyEvent.
+ //
+ // This is not a good approach and we should fix things up so that only
+ // one postEvent is needed.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+
+ if (self->mSuspendNativeCount <= 0) {
+ ++self->mNativeEventCallbackDepth;
+ self->NativeEventCallback();
+ --self->mNativeEventCallbackDepth;
+ } else {
+ self->mSkippedNativeCallback = true;
+ }
+
+ if (self->mTerminated) {
+ // Still needed to avoid crashes on quit in most Mochitests.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+ // Normally every call to ScheduleNativeEventCallback() results in
+ // exactly one call to ProcessGeckoEvents(). So each Release() here
+ // normally balances exactly one AddRef() in ScheduleNativeEventCallback().
+ // But if Exit() is called just after ScheduleNativeEventCallback(), the
+ // corresponding call to ProcessGeckoEvents() will never happen. We check
+ // for this possibility in two different places -- here and in Exit()
+ // itself. If we find here that Exit() has been called (that mTerminated
+ // is true), it's because we've been called recursively, that Exit() was
+ // called from self->NativeEventCallback() above, and that we're unwinding
+ // the recursion. In this case we'll never be called again, and we balance
+ // here any extra calls to ScheduleNativeEventCallback().
+ //
+ // When ProcessGeckoEvents() is called recursively, it's because of a
+ // call to ScheduleNativeEventCallback() from NativeEventCallback(). We
+ // balance the "extra" AddRefs here (rather than always in Exit()) in order
+ // to ensure that 'self' stays alive until the end of this method. We also
+ // make sure not to finish the balancing until all the recursion has been
+ // unwound.
+ if (self->mTerminated) {
+ int32_t releaseCount = 0;
+ if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) {
+ releaseCount =
+ PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, self->mNativeEventCallbackDepth);
+ }
+ while (releaseCount-- > self->mNativeEventCallbackDepth) self->Release();
+ } else {
+ // As best we can tell, every call to ProcessGeckoEvents() is triggered
+ // by a call to ScheduleNativeEventCallback(). But we've seen a few
+ // (non-reproducible) cases of double-frees that *might* have been caused
+ // by spontaneous calls (from the OS) to ProcessGeckoEvents(). So we
+ // deal with that possibility here.
+ if (PR_ATOMIC_DECREMENT(&self->mNativeEventScheduledDepth) < 0) {
+ PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, 0);
+ NS_WARNING("Spontaneous call to ProcessGeckoEvents()!");
+ } else {
+ self->Release();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// WillTerminate
+//
+// Called by the AppShellDelegate when an NSApplicationWillTerminate
+// notification is posted. After this method is called, native events should
+// no longer be processed. The NSApplicationWillTerminate notification is
+// only posted when [NSApp terminate:] is called, which doesn't happen on a
+// "normal" application quit.
+//
+// public
+void nsAppShell::WillTerminate() {
+ if (mTerminated) return;
+
+ // Make sure that the nsAppExitEvent posted by nsAppStartup::Quit() (called
+ // from [MacApplicationDelegate applicationShouldTerminate:]) gets run.
+ NS_ProcessPendingEvents(NS_GetCurrentThread());
+
+ mTerminated = true;
+}
+
+// ScheduleNativeEventCallback
+//
+// Called (possibly on a non-main thread) when Gecko has an event that
+// needs to be processed. The Gecko event needs to be processed on the
+// main thread, so the native run loop must be interrupted.
+//
+// In nsBaseAppShell.cpp, the mNativeEventPending variable is used to
+// ensure that ScheduleNativeEventCallback() is called no more than once
+// per call to NativeEventCallback(). ProcessGeckoEvents() can skip its
+// call to NativeEventCallback() if processing of Gecko events by native
+// means is suspended (using nsIAppShell::SuspendNative()), which will
+// suspend calls from nsBaseAppShell::OnDispatchedEvent() to
+// ScheduleNativeEventCallback(). But when Gecko event processing by
+// native means is resumed (in ResumeNative()), an extra call is made to
+// ScheduleNativeEventCallback() (from ResumeNative()). This triggers
+// another call to ProcessGeckoEvents(), which calls NativeEventCallback(),
+// and nsBaseAppShell::OnDispatchedEvent() resumes calling
+// ScheduleNativeEventCallback().
+//
+// protected virtual
+void nsAppShell::ScheduleNativeEventCallback() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTerminated) return;
+
+ // Each AddRef() here is normally balanced by exactly one Release() in
+ // ProcessGeckoEvents(). But there are exceptions, for which see
+ // ProcessGeckoEvents() and Exit().
+ NS_ADDREF_THIS();
+ PR_ATOMIC_INCREMENT(&mNativeEventScheduledDepth);
+
+ // This will invoke ProcessGeckoEvents on the main thread.
+ ::CFRunLoopSourceSignal(mCFRunLoopSource);
+ ::CFRunLoopWakeUp(mCFRunLoop);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Undocumented Cocoa Event Manager function, present in the same form since
+// at least OS X 10.6.
+extern "C" EventAttributes GetEventAttributes(EventRef inEvent);
+
+// ProcessNextNativeEvent
+//
+// If aMayWait is false, process a single native event. If it is true, run
+// the native run loop until stopped by ProcessGeckoEvents.
+//
+// Returns true if more events are waiting in the native event queue.
+//
+// protected virtual
+bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) {
+ bool moreEvents = false;
+
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ bool eventProcessed = false;
+ NSString* currentMode = nil;
+
+ if (mTerminated) return false;
+
+ // Do not call -[NSApplication nextEventMatchingMask:...] when we're trying to close a native
+ // menu. Doing so could confuse the NSMenu's closing mechanism. Instead, we try to unwind the
+ // stack as quickly as possible and return to the parent event loop. At that point, native events
+ // will be processed.
+ if (MOZMenuOpeningCoordinator.needToUnwindForMenuClosing) {
+ return false;
+ }
+
+ bool wasRunningEventLoop = mRunningEventLoop;
+ mRunningEventLoop = aMayWait;
+ NSDate* waitUntil = nil;
+ if (aMayWait) waitUntil = [NSDate distantFuture];
+
+ NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
+
+ EventQueueRef currentEventQueue = GetCurrentEventQueue();
+
+ if (aMayWait) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // Only call -[NSApp sendEvent:] (and indirectly send user-input events to
+ // Gecko) if aMayWait is true. Tbis ensures most calls to -[NSApp
+ // sendEvent:] happen under nsAppShell::Run(), at the lowest level of
+ // recursion -- thereby making it less likely Gecko will process user-input
+ // events in the wrong order or skip some of them. It also avoids eating
+ // too much CPU in nsBaseAppShell::OnProcessNextEvent() (which calls
+ // us) -- thereby avoiding the starvation of nsIRunnable events in
+ // nsThread::ProcessNextEvent(). For more information see bug 996848.
+ do {
+ // No autorelease pool is provided here, because OnProcessNextEvent
+ // and AfterProcessNextEvent are responsible for maintaining it.
+ NS_ASSERTION(mAutoreleasePools && ::CFArrayGetCount(mAutoreleasePools),
+ "No autorelease pool for native event");
+
+ if (aMayWait) {
+ currentMode = [currentRunLoop currentMode];
+ if (!currentMode) currentMode = NSDefaultRunLoopMode;
+ NSEvent* nextEvent = [NSApp nextEventMatchingMask:NSEventMaskAny
+ untilDate:waitUntil
+ inMode:currentMode
+ dequeue:YES];
+ if (nextEvent) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ [NSApp sendEvent:nextEvent];
+ eventProcessed = true;
+ }
+ } else {
+ // In at least 10.15, AcquireFirstMatchingEventInQueue will move 1
+ // CGEvent from the CGEvent queue into the Carbon event queue. Unfortunately,
+ // once an event has been moved to the Carbon event queue it's no longer a
+ // candidate for coalescing. This means that even if we don't remove the
+ // event from the queue, just calling AcquireFirstMatchingEventInQueue can
+ // cause behaviour change. Prior to bug 1690687 landing, the event that we got
+ // from AcquireFirstMatchingEventInQueue was often our own ApplicationDefined
+ // event. However, once we stopped posting that event on every Gecko
+ // event we're much more likely to get a CGEvent. When we have a high
+ // amount of load on the main thread, we end up alternating between Gecko
+ // events and native events. Without CGEvent coalescing, the native
+ // event events can accumulate in the Carbon event queue which will
+ // manifest as laggy scrolling.
+#if 1
+ eventProcessed = false;
+ break;
+#else
+ // AcquireFirstMatchingEventInQueue() doesn't spin the (native) event
+ // loop, though it does queue up any newly available events from the
+ // window server.
+ EventRef currentEvent =
+ AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL, kEventQueueOptionsNone);
+ if (!currentEvent) {
+ continue;
+ }
+ EventAttributes attrs = GetEventAttributes(currentEvent);
+ UInt32 eventKind = GetEventKind(currentEvent);
+ UInt32 eventClass = GetEventClass(currentEvent);
+ bool osCocoaEvent =
+ ((eventClass == 'appl') || (eventClass == kEventClassAppleEvent) ||
+ ((eventClass == 'cgs ') && (eventKind != NSEventTypeApplicationDefined)));
+ // If attrs is kEventAttributeUserEvent or kEventAttributeMonitored
+ // (i.e. a user input event), we shouldn't process it here while
+ // aMayWait is false. Likewise if currentEvent will eventually be
+ // turned into an OS-defined Cocoa event, or otherwise needs AppKit
+ // processing. Doing otherwise risks doing too much work here, and
+ // preventing the event from being properly processed by the AppKit
+ // framework.
+ if ((attrs != kEventAttributeNone) || osCocoaEvent) {
+ // Since we can't process the next event here (while aMayWait is false),
+ // we want moreEvents to be false on return.
+ eventProcessed = false;
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ break;
+ }
+ // This call to RetainEvent() matches a call to ReleaseEvent() in
+ // RemoveEventFromQueue() below.
+ RetainEvent(currentEvent);
+ RemoveEventFromQueue(currentEventQueue, currentEvent);
+ EventTargetRef eventDispatcherTarget = GetEventDispatcherTarget();
+ SendEventToEventTarget(currentEvent, eventDispatcherTarget);
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ eventProcessed = true;
+#endif
+ }
+ } while (mRunningEventLoop);
+
+ if (eventProcessed) {
+ moreEvents = (AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL,
+ kEventQueueOptionsNone) != NULL);
+ }
+
+ mRunningEventLoop = wasRunningEventLoop;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+
+ if (!moreEvents) {
+ nsChildView::UpdateCurrentInputEventCount();
+ }
+
+ return moreEvents;
+}
+
+// Attempt to work around bug 1801419 by loading and initializing the
+// SidecarCore private framework as the app shell starts up. This normally
+// happens on demand, the first time any Cmd-key combination is pressed, and
+// sometimes triggers crashes, caused by an Apple bug. We hope that doing it
+// now, and somewhat more simply, will avoid the crashes. They happen
+// (intermittently) when SidecarCore code tries to access C strings in special
+// sections of its own __TEXT segment, and triggers fatal page faults (which
+// is Apple's bug). Many of the C strings are part of the Objective-C class
+// hierarchy (class names and so forth). We hope that adding them to this
+// hierarchy will "pin" them in place -- so they'll rarely, if ever, be paged
+// out again. Bug 1801419's crashes happen much more often on macOS 13
+// (Ventura) than on other versions of macOS. So we only use this hack on
+// macOS 13 and up.
+static void PinSidecarCoreTextCStringSections() {
+ if (!dlopen("/System/Library/PrivateFrameworks/SidecarCore.framework/SidecarCore", RTLD_LAZY)) {
+ return;
+ }
+
+ // Explicitly run the most basic part of the initialization code that
+ // normally runs automatically on the first Cmd-key combination.
+ Class displayManagerClass = NSClassFromString(@"SidecarDisplayManager");
+ if ([displayManagerClass respondsToSelector:@selector(sharedManager)]) {
+ id sharedManager = [displayManagerClass performSelector:@selector(sharedManager)];
+ if ([sharedManager respondsToSelector:@selector(devices)]) {
+ [sharedManager performSelector:@selector(devices)];
+ }
+ }
+}
+
+// Run
+//
+// Overrides the base class's Run() method to call [NSApp run] (which spins
+// the native run loop until the application quits). Since (unlike the base
+// class's Run() method) we don't process any Gecko events here, they need
+// to be processed elsewhere (in NativeEventCallback(), called from
+// ProcessGeckoEvents()).
+//
+// Camino called [NSApp run] on its own (via NSApplicationMain()), and so
+// didn't call nsAppShell::Run().
+//
+// public
+NS_IMETHODIMP
+nsAppShell::Run(void) {
+ NS_ASSERTION(!mStarted, "nsAppShell::Run() called multiple times");
+ if (mStarted || mTerminated) return NS_OK;
+
+ mStarted = true;
+
+ if (XRE_IsParentProcess()) {
+ if (nsCocoaFeatures::OnVenturaOrLater()) {
+ PinSidecarCoreTextCStringSections();
+ }
+ AddScreenWakeLockListener();
+ }
+
+ // We use the native Gecko event loop in content processes.
+ nsresult rv = NS_OK;
+ if (XRE_UseNativeEventProcessing()) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [NSApp run];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ } else {
+ rv = nsBaseAppShell::Run();
+ }
+
+ if (XRE_IsParentProcess()) {
+ RemoveScreenWakeLockListener();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppShell::Exit(void) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // This method is currently called more than once -- from (according to
+ // mento) an nsAppExitEvent dispatched by nsAppStartup::Quit() and from an
+ // XPCOM shutdown notification that nsBaseAppShell has registered to
+ // receive. So we need to ensure that multiple calls won't break anything.
+ // But we should also complain about it (since it isn't quite kosher).
+ if (mTerminated) {
+ NS_WARNING("nsAppShell::Exit() called redundantly");
+ return NS_OK;
+ }
+
+ mTerminated = true;
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ nsSandboxViolationSink::Stop();
+#endif
+
+ // Quoting from Apple's doc on the [NSApplication stop:] method (from their
+ // doc on the NSApplication class): "If this method is invoked during a
+ // modal event loop, it will break that loop but not the main event loop."
+ // nsAppShell::Exit() shouldn't be called from a modal event loop. So if
+ // it is we complain about it (to users of debug builds) and call [NSApp
+ // stop:] one extra time. (I'm not sure if modal event loops can be nested
+ // -- Apple's docs don't say one way or the other. But the return value
+ // of [NSApp _isRunningModal] doesn't change immediately after a call to
+ // [NSApp stop:], so we have to assume that one extra call to [NSApp stop:]
+ // will do the job.)
+ BOOL cocoaModal = [NSApp _isRunningModal];
+ NS_ASSERTION(!cocoaModal, "Don't call nsAppShell::Exit() from a modal event loop!");
+ if (cocoaModal) [NSApp stop:nullptr];
+ [NSApp stop:nullptr];
+
+ // A call to Exit() just after a call to ScheduleNativeEventCallback()
+ // prevents the (normally) matching call to ProcessGeckoEvents() from
+ // happening. If we've been called from ProcessGeckoEvents() (as usually
+ // happens), we take care of it there. But if we have an unbalanced call
+ // to ScheduleNativeEventCallback() and ProcessGeckoEvents() isn't on the
+ // stack, we need to take care of the problem here.
+ if (!mNativeEventCallbackDepth && mNativeEventScheduledDepth) {
+ int32_t releaseCount = PR_ATOMIC_SET(&mNativeEventScheduledDepth, 0);
+ while (releaseCount-- > 0) NS_RELEASE_THIS();
+ }
+
+ return nsBaseAppShell::Exit();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// OnProcessNextEvent
+//
+// This nsIThreadObserver method is called prior to processing an event.
+// Set up an autorelease pool that will service any autoreleased Cocoa
+// objects during this event. This includes native events processed by
+// ProcessNextNativeEvent. The autorelease pool will be popped by
+// AfterProcessNextEvent, it is important for these two methods to be
+// tightly coupled.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::OnProcessNextEvent(nsIThreadInternal* aThread, bool aMayWait) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ASSERTION(mAutoreleasePools, "No stack on which to store autorelease pool");
+
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+ ::CFArrayAppendValue(mAutoreleasePools, pool);
+
+ return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// AfterProcessNextEvent
+//
+// This nsIThreadObserver method is called after event processing is complete.
+// The Cocoa implementation cleans up the autorelease pool create by the
+// previous OnProcessNextEvent call.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::AfterProcessNextEvent(nsIThreadInternal* aThread, bool aEventWasProcessed) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CFIndex count = ::CFArrayGetCount(mAutoreleasePools);
+
+ NS_ASSERTION(mAutoreleasePools && count, "Processed an event, but there's no autorelease pool?");
+
+ const NSAutoreleasePool* pool =
+ static_cast<const NSAutoreleasePool*>(::CFArrayGetValueAtIndex(mAutoreleasePools, count - 1));
+ ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1);
+ [pool release];
+
+ return nsBaseAppShell::AfterProcessNextEvent(aThread, aEventWasProcessed);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsAppShell::InitMemoryPressureObserver() {
+ // Testing shows that sometimes the memory pressure event is not fired for
+ // over a minute after the memory pressure change is reflected in sysctl
+ // values. Hence this may need to be augmented with polling of the memory
+ // pressure sysctls for lower latency reactions to OS memory pressure. This
+ // was also observed when using DISPATCH_QUEUE_PRIORITY_HIGH.
+ mMemoryPressureSource =
+ dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0,
+ DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN |
+ DISPATCH_MEMORYPRESSURE_CRITICAL,
+ dispatch_get_main_queue());
+
+ dispatch_source_set_event_handler(mMemoryPressureSource, ^{
+ dispatch_source_memorypressure_flags_t pressureLevel =
+ dispatch_source_get_data(mMemoryPressureSource);
+ nsAppShell::OnMemoryPressureChanged(pressureLevel);
+ });
+
+ dispatch_resume(mMemoryPressureSource);
+
+ // Initialize the memory watcher.
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+}
+
+void nsAppShell::OnMemoryPressureChanged(dispatch_source_memorypressure_flags_t aPressureLevel) {
+ // The memory pressure dispatch source is created (above) with
+ // dispatch_get_main_queue() which always fires on the main thread.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MacMemoryPressureLevel geckoPressureLevel;
+ switch (aPressureLevel) {
+ case DISPATCH_MEMORYPRESSURE_NORMAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eNormal;
+ break;
+ case DISPATCH_MEMORYPRESSURE_WARN:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eWarning;
+ break;
+ case DISPATCH_MEMORYPRESSURE_CRITICAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eCritical;
+ break;
+ default:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eUnexpected;
+ }
+
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+ watcher->OnMemoryPressureChanged(geckoPressureLevel);
+}
+
+// AppShellDelegate implementation
+
+@implementation AppShellDelegate
+// initWithAppShell:
+//
+// Constructs the AppShellDelegate object
+- (id)initWithAppShell:(nsAppShell*)aAppShell {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [self init])) {
+ mAppShell = aAppShell;
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationWillTerminate:)
+ name:NSApplicationWillTerminateNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationDidBecomeActive:)
+ name:NSApplicationDidBecomeActiveNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(timezoneChanged:)
+ name:NSSystemTimeZoneDidChangeNotification
+ object:nil];
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationWillTerminate:
+//
+// Notify the nsAppShell that native event processing should be discontinued.
+- (void)applicationWillTerminate:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mAppShell->WillTerminate();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationDidBecomeActive
+//
+// Make sure TextInputHandler::sLastModifierState is updated when we become
+// active (since we won't have received [ChildView flagsChanged:] messages
+// while inactive).
+- (void)applicationDidBecomeActive:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // [NSEvent modifierFlags] is valid on every kind of event, so we don't need
+ // to worry about getting an NSInternalInconsistencyException here.
+ NSEvent* currentEvent = [NSApp currentEvent];
+ if (currentEvent) {
+ TextInputHandler::sLastModifierState =
+ [currentEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->NotifyObservers(nullptr, NS_WIDGET_MAC_APP_ACTIVATE_OBSERVER_TOPIC, nullptr);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)timezoneChanged:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsBaseAppShell::OnSystemTimezoneChange();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)shouldSaveApplicationState:(NSCoder*)coder {
+ return YES;
+}
+
+- (BOOL)shouldRestoreApplicationState:(NSCoder*)coder {
+ return YES;
+}
+
+@end
+
+// We hook terminate: in order to make OS-initiated termination work nicely
+// with Gecko's shutdown sequence. (Two ways to trigger OS-initiated
+// termination: 1) Quit from the Dock menu; 2) Log out from (or shut down)
+// your computer while the browser is active.)
+@interface NSApplication (MethodSwizzling)
+- (void)nsAppShell_NSApplication_terminate:(id)sender;
+@end
+
+@implementation NSApplication (MethodSwizzling)
+
+// Called by the OS after [MacApplicationDelegate applicationShouldTerminate:]
+// has returned NSTerminateNow. This method "subclasses" and replaces the
+// OS's original implementation. The only thing the orginal method does which
+// we need is that it posts NSApplicationWillTerminateNotification. Everything
+// else is unneeded (because it's handled elsewhere), or actively interferes
+// with Gecko's shutdown sequence. For example the original terminate: method
+// causes the app to exit() inside [NSApp run] (called from nsAppShell::Run()
+// above), which means that nothing runs after the call to nsAppStartup::Run()
+// in XRE_Main(), which in particular means that ScopedXPCOMStartup's destructor
+// and NS_ShutdownXPCOM() never get called.
+- (void)nsAppShell_NSApplication_terminate:(id)sender {
+ [[NSNotificationCenter defaultCenter] postNotificationName:NSApplicationWillTerminateNotification
+ object:NSApp];
+}
+
+@end
diff --git a/widget/cocoa/nsBidiKeyboard.h b/widget/cocoa/nsBidiKeyboard.h
new file mode 100644
index 0000000000..3a9a6fe2fb
--- /dev/null
+++ b/widget/cocoa/nsBidiKeyboard.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsBidiKeyboard_h_
+#define nsBidiKeyboard_h_
+
+#include "nsIBidiKeyboard.h"
+
+class nsBidiKeyboard : public nsIBidiKeyboard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBIDIKEYBOARD
+
+ nsBidiKeyboard();
+
+ protected:
+ virtual ~nsBidiKeyboard();
+};
+
+#endif // nsBidiKeyboard_h_
diff --git a/widget/cocoa/nsBidiKeyboard.mm b/widget/cocoa/nsBidiKeyboard.mm
new file mode 100644
index 0000000000..4193bdf6f0
--- /dev/null
+++ b/widget/cocoa/nsBidiKeyboard.mm
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsBidiKeyboard.h"
+#include "nsCocoaUtils.h"
+#include "TextInputHandler.h"
+#include "nsIWidget.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla::widget;
+
+NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard)
+
+nsBidiKeyboard::nsBidiKeyboard() : nsIBidiKeyboard() { Reset(); }
+
+nsBidiKeyboard::~nsBidiKeyboard() {}
+
+NS_IMETHODIMP nsBidiKeyboard::Reset() { return NS_OK; }
+
+NS_IMETHODIMP nsBidiKeyboard::IsLangRTL(bool* aIsRTL) {
+ *aIsRTL = TISInputSourceWrapper::CurrentInputSource().IsForRTLLanguage();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) {
+ // not implemented yet
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// static
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() {
+ return do_AddRef(new nsBidiKeyboard());
+}
diff --git a/widget/cocoa/nsChangeObserver.h b/widget/cocoa/nsChangeObserver.h
new file mode 100644
index 0000000000..aacf837bb5
--- /dev/null
+++ b/widget/cocoa/nsChangeObserver.h
@@ -0,0 +1,71 @@
+/* -*- Mode: IDL; tab-width: 2; 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/. */
+
+#ifndef nsChangeObserver_h_
+#define nsChangeObserver_h_
+
+class nsIContent;
+class nsAtom;
+namespace mozilla {
+namespace dom {
+class Document;
+}
+} // namespace mozilla
+
+#define NS_DECL_CHANGEOBSERVER \
+ void ObserveAttributeChanged(mozilla::dom::Document* aDocument, \
+ nsIContent* aContent, nsAtom* aAttribute) \
+ override; \
+ void ObserveContentRemoved(mozilla::dom::Document* aDocument, \
+ nsIContent* aContainer, nsIContent* aChild, \
+ nsIContent* aPreviousChild) override; \
+ void ObserveContentInserted(mozilla::dom::Document* aDocument, \
+ nsIContent* aContainer, nsIContent* aChild) \
+ override;
+
+// Something that wants to be alerted to changes in attributes or changes in
+// its corresponding content object.
+//
+// This interface is used by our menu code so we only have to have one
+// nsIMutationObserver per menu subtree root (e.g. per menubar).
+//
+// Any class that implements this interface must take care to unregister itself
+// on deletion.
+//
+// XXXmstange The methods below use nsIContent*. Eventually, the should be
+// converted to use mozilla::dom::Element* instead.
+class nsChangeObserver {
+ public:
+ // Called when the attribute aAttribute on the element aContent has changed.
+ // Only if aContent is being observed by this nsChangeObserver.
+ virtual void ObserveAttributeChanged(mozilla::dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) = 0;
+
+ // Called when aChild has been removed from its parent aContainer.
+ // aPreviousSibling is the old previous sibling of aChild.
+ // aContainer is always the old parent node of aChild and of aPreviousSibling.
+ // Only called if aContainer or aContainer's parent node are being observed
+ // by this nsChangeObserver.
+ // In other words: If you observe an element, ObserveContentRemoved is called
+ // if that element's children and grandchildren are removed. NOT if the
+ // observed element itself is removed.
+ virtual void ObserveContentRemoved(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer, nsIContent* aChild,
+ nsIContent* aPreviousSibling) = 0;
+
+ // Called when aChild has been inserted into its new parent aContainer.
+ // Only called if aContainer or aContainer's parent node are being observed
+ // by this nsChangeObserver.
+ // In other words: If you observe an element, ObserveContentInserted is called
+ // if that element receives a new child or grandchild. NOT if the observed
+ // element itself is inserted anywhere.
+ virtual void ObserveContentInserted(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) = 0;
+};
+
+#endif // nsChangeObserver_h_
diff --git a/widget/cocoa/nsChildView.h b/widget/cocoa/nsChildView.h
new file mode 100644
index 0000000000..b8095b991e
--- /dev/null
+++ b/widget/cocoa/nsChildView.h
@@ -0,0 +1,591 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsChildView_h_
+#define nsChildView_h_
+
+// formal protocols
+#include "mozView.h"
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/LocalAccessible.h"
+# include "mozAccessibleProtocol.h"
+#endif
+
+#include "nsISupports.h"
+#include "nsBaseWidget.h"
+#include "nsIWeakReferenceUtils.h"
+#include "TextInputHandler.h"
+#include "nsCocoaUtils.h"
+#include "gfxQuartzSurface.h"
+#include "GLContextTypes.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/Mutex.h"
+#include "nsRegion.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+#include "nsString.h"
+#include "nsIDragService.h"
+#include "ViewRegion.h"
+#include "CFTypeRefPtr.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#import <AppKit/NSOpenGL.h>
+
+class nsChildView;
+class nsCocoaWindow;
+
+namespace {
+class GLPresenter;
+} // namespace
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class InputData;
+class PanGestureInput;
+class VibrancyManager;
+namespace layers {
+class GLManager;
+class IAPZCTreeManager;
+class NativeLayerRootCA;
+class NativeLayerCA;
+} // namespace layers
+namespace widget {
+class WidgetRenderingContext;
+} // namespace widget
+} // namespace mozilla
+
+@class PixelHostingView;
+
+@interface NSEvent (Undocumented)
+
+// Return Cocoa event's corresponding Carbon event. Not initialized (on
+// synthetic events) until the OS actually "sends" the event. This method
+// has been present in the same form since at least OS X 10.2.8.
+- (EventRef)_eventRef;
+
+// stage From 10.10.3 for force touch event
+@property(readonly) NSInteger stage;
+
+@end
+
+@interface NSView (Undocumented)
+
+// Undocumented method of one or more of NSFrameView's subclasses. Called
+// when one or more of the titlebar buttons needs to be repositioned, to
+// disappear, or to reappear (say if the window's style changes). If
+// 'redisplay' is true, the entire titlebar (the window's top 22 pixels) is
+// marked as needing redisplay. This method has been present in the same
+// format since at least OS X 10.5.
+- (void)_tileTitlebarAndRedisplay:(BOOL)redisplay;
+
+// The following undocumented methods are used to work around bug 1069658,
+// which is an Apple bug or design flaw that effects Yosemite. None of them
+// were present prior to Yosemite (OS X 10.10).
+- (NSView*)titlebarView; // Method of NSThemeFrame
+- (NSView*)titlebarContainerView; // Method of NSThemeFrame
+- (BOOL)transparent; // Method of NSTitlebarView and NSTitlebarContainerView
+- (void)setTransparent:(BOOL)transparent; // Method of NSTitlebarView and
+ // NSTitlebarContainerView
+
+// Available since 10.7.4:
+- (void)viewDidChangeBackingProperties;
+@end
+
+@interface ChildView : NSView <
+#ifdef ACCESSIBILITY
+ mozAccessible,
+#endif
+ mozView,
+ NSTextInputClient,
+ NSDraggingSource,
+ NSDraggingDestination,
+ NSPasteboardItemDataProvider> {
+ @private
+ // the nsChildView that created the view. It retains this NSView, so
+ // the link back to it must be weak.
+ nsChildView* mGeckoChild;
+
+ // Text input handler for mGeckoChild and us. Note that this is a weak
+ // reference. Ideally, this should be a strong reference but a ChildView
+ // object can live longer than the mGeckoChild that owns it. And if
+ // mTextInputHandler were a strong reference, this would make it difficult
+ // for Gecko's leak detector to detect leaked TextInputHandler objects.
+ // This is initialized by [mozView installTextInputHandler:aHandler] and
+ // cleared by [mozView uninstallTextInputHandler].
+ mozilla::widget::TextInputHandler* mTextInputHandler; // [WEAK]
+
+ // when mouseDown: is called, we store its event here (strong)
+ NSEvent* mLastMouseDownEvent;
+
+ // Needed for IME support in e10s mode. Strong.
+ NSEvent* mLastKeyDownEvent;
+
+ // Whether the last mouse down event was blocked from Gecko.
+ BOOL mBlockedLastMouseDown;
+
+ // when acceptsFirstMouse: is called, we store the event here (strong)
+ NSEvent* mClickThroughMouseDownEvent;
+
+ // WheelStart/Stop events should always come in pairs. This BOOL records the
+ // last received event so that, when we receive one of the events, we make sure
+ // to send its pair event first, in case we didn't yet for any reason.
+ BOOL mExpectingWheelStop;
+
+ // Whether we're inside updateRootCALayer at the moment.
+ BOOL mIsUpdatingLayer;
+
+ // Holds our drag service across multiple drag calls. The reference to the
+ // service is obtained when the mouse enters the view and is released when
+ // the mouse exits or there is a drop. This prevents us from having to
+ // re-establish the connection to the service manager many times per second
+ // when handling |draggingUpdated:| messages.
+ nsIDragService* mDragService;
+
+ // Gestures support
+ //
+ // mGestureState is used to detect when Cocoa has called both
+ // magnifyWithEvent and rotateWithEvent within the same
+ // beginGestureWithEvent and endGestureWithEvent sequence. We
+ // discard the spurious gesture event so as not to confuse Gecko.
+ //
+ // mCumulativeRotation keeps track of the total amount of rotation
+ // performed during a rotate gesture so we can send that value with
+ // the final MozRotateGesture event.
+ enum {
+ eGestureState_None,
+ eGestureState_StartGesture,
+ eGestureState_MagnifyGesture,
+ eGestureState_RotateGesture
+ } mGestureState;
+ float mCumulativeRotation;
+
+#ifdef __LP64__
+ // Support for fluid swipe tracking.
+ BOOL* mCancelSwipeAnimation;
+#endif
+
+ // Whether this uses off-main-thread compositing.
+ BOOL mUsingOMTCompositor;
+
+ // Subviews of self, which act as container views for vibrancy views and
+ // non-draggable views.
+ NSView* mVibrancyViewsContainer; // [STRONG]
+ NSView* mNonDraggableViewsContainer; // [STRONG]
+
+ // The layer-backed view that hosts our drawing. Always non-null.
+ // This is a subview of self so that it can be ordered on top of mVibrancyViewsContainer.
+ PixelHostingView* mPixelHostingView;
+
+ // The CALayer that wraps Gecko's rendered contents. It's a sublayer of
+ // mPixelHostingView's backing layer. Always non-null.
+ CALayer* mRootCALayer; // [STRONG]
+
+ // Last pressure stage by trackpad's force click
+ NSInteger mLastPressureStage;
+}
+
+// class initialization
++ (void)initialize;
+
++ (void)registerViewForDraggedTypes:(NSView*)aView;
+
+// these are sent to the first responder when the window key status changes
+- (void)viewsWindowDidBecomeKey;
+- (void)viewsWindowDidResignKey;
+
+// Stop NSView hierarchy being changed during [ChildView drawRect:]
+- (void)delayedTearDown;
+
+- (void)handleMouseMoved:(NSEvent*)aEvent;
+
+- (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent
+ enter:(BOOL)aEnter
+ exitFrom:(mozilla::WidgetMouseEvent::ExitFrom)aExitFrom;
+
+// Call this during operations that will likely trigger a main thread
+// CoreAnimation paint of the window, during which Gecko should do its own
+// painting and present the results atomically with that main thread transaction.
+// This method will suspend off-thread window updates so that the upcoming paint
+// can be atomic, and mark the layer as needing display so that
+// HandleMainThreadCATransaction gets called and Gecko gets a chance to paint.
+- (void)ensureNextCompositeIsAtomicWithMainThreadPaint;
+
+- (NSView*)vibrancyViewsContainer;
+- (NSView*)nonDraggableViewsContainer;
+- (NSView*)pixelHostingView;
+
+- (BOOL)isCoveringTitlebar;
+
+- (void)viewWillStartLiveResize;
+- (void)viewDidEndLiveResize;
+
+/*
+ * Gestures support
+ *
+ * The prototypes swipeWithEvent, beginGestureWithEvent, smartMagnifyWithEvent,
+ * rotateWithEvent and endGestureWithEvent were obtained from the following
+ * links:
+ * https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/Reference/Reference.html
+ * https://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKit.html
+ */
+- (void)swipeWithEvent:(NSEvent*)anEvent;
+- (void)beginGestureWithEvent:(NSEvent*)anEvent;
+- (void)magnifyWithEvent:(NSEvent*)anEvent;
+- (void)smartMagnifyWithEvent:(NSEvent*)anEvent;
+- (void)rotateWithEvent:(NSEvent*)anEvent;
+- (void)endGestureWithEvent:(NSEvent*)anEvent;
+
+- (void)scrollWheel:(NSEvent*)anEvent;
+
+- (void)setUsingOMTCompositor:(BOOL)aUseOMTC;
+
+- (NSEvent*)lastKeyDownEvent;
+
++ (uint32_t)sUniqueKeyEventId;
+
++ (NSMutableDictionary*)sNativeKeyEventsMap;
+@end
+
+class ChildViewMouseTracker {
+ public:
+ static void MouseMoved(NSEvent* aEvent);
+ static void MouseScrolled(NSEvent* aEvent);
+ static void OnDestroyView(ChildView* aView);
+ static void OnDestroyWindow(NSWindow* aWindow);
+ static BOOL WindowAcceptsEvent(NSWindow* aWindow, NSEvent* aEvent, ChildView* aView,
+ BOOL isClickThrough = NO);
+ static void MouseExitedWindow(NSEvent* aEvent);
+ static void MouseEnteredWindow(NSEvent* aEvent);
+ static void NativeMenuOpened();
+ static void NativeMenuClosed();
+ static void ReEvaluateMouseEnterState(NSEvent* aEvent = nil, ChildView* aOldView = nil);
+ static void ResendLastMouseMoveEvent();
+ static ChildView* ViewForEvent(NSEvent* aEvent);
+
+ static ChildView* sLastMouseEventView;
+ static NSEvent* sLastMouseMoveEvent;
+ static NSWindow* sWindowUnderMouse;
+ static NSPoint sLastScrollEventScreenLocation;
+};
+
+//-------------------------------------------------------------------------
+//
+// nsChildView
+//
+//-------------------------------------------------------------------------
+
+class nsChildView final : public nsBaseWidget {
+ private:
+ typedef nsBaseWidget Inherited;
+ typedef mozilla::layers::IAPZCTreeManager IAPZCTreeManager;
+
+ public:
+ nsChildView();
+
+ // nsIWidget interface
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+ virtual bool IsVisible() const override;
+
+ virtual void SetParent(nsIWidget* aNewParent) override;
+ virtual nsIWidget* GetParent(void) override;
+ virtual float GetDPI() override;
+
+ virtual void Move(double aX, double aY) override;
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight, bool aRepaint) override;
+
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override { mSizeMode = aMode; }
+
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntRect GetBounds() override;
+ virtual LayoutDeviceIntRect GetClientBounds() override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+
+ // Refresh mBounds with up-to-date values from [mView frame].
+ // Only called if this nsChildView is the popup content view of a popup window.
+ // For popup windows, the nsIWidget interface to Gecko is provided by
+ // nsCocoaWindow, not by nsChildView. So nsCocoaWindow manages resize requests
+ // from Gecko, fires resize events, and resizes the native NSWindow and NSView.
+ void UpdateBoundsFromView();
+
+ // Returns the "backing scale factor" of the view's window, which is the
+ // ratio of pixels in the window's backing store to Cocoa points. Prior to
+ // HiDPI support in OS X 10.7, this was always 1.0, but in HiDPI mode it
+ // will be 2.0 (and might potentially other values as screen resolutions
+ // evolve). This gives the relationship between what Gecko calls "device
+ // pixels" and the Cocoa "points" coordinate system.
+ CGFloat BackingScaleFactor() const;
+
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() final {
+ return mozilla::DesktopToLayoutDeviceScale(BackingScaleFactor());
+ }
+
+ // Call if the window's backing scale factor changes - i.e., it is moved
+ // between HiDPI and non-HiDPI screens
+ void BackingScaleFactorChanged();
+
+ virtual double GetDefaultScaleInternal() override;
+
+ virtual int32_t RoundsWidgetCoordinatesTo() override;
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ void EnsureContentLayerForMainThreadPainting();
+
+ virtual void* GetNativeData(uint32_t aDataType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual bool ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) override { return false; }
+
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, nsEventStatus& aStatus) override;
+
+ virtual bool WidgetTypeSupportsAcceleration() override;
+ virtual bool ShouldUseOffMainThreadCompositing() override;
+
+ virtual void SetCursor(const Cursor&) override;
+
+ virtual nsresult SetTitle(const nsAString& title) override;
+
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override;
+
+ virtual bool HasPendingInputEvent() override;
+
+ bool SendEventToNativeMenuSystem(NSEvent* aEvent);
+ virtual void PostHandleKeyEvent(mozilla::WidgetKeyboardEvent* aEvent) override;
+ virtual nsresult ActivateNativeMenuItemAt(const nsAString& indexString) override;
+ virtual nsresult ForceUpdateNativeMenuAt(const nsAString& indexString) override;
+ [[nodiscard]] virtual nsresult GetSelectionAsPlaintext(nsAString& aResult) override;
+
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override;
+ virtual TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ mozilla::WidgetKeyboardEvent& aEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ virtual void SuppressAnimation(bool aSuppress) override;
+
+ virtual nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(aPoint, NativeMouseMessage::Move,
+ mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ }
+ virtual nsresult SynthesizeNativeMouseScrollEvent(LayoutDeviceIntPoint aPoint,
+ uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ,
+ uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchpadDoubleTap(LayoutDeviceIntPoint aPoint,
+ uint32_t aModifierFlags) override;
+
+ // Mac specific methods
+ void WillPaintWindow();
+ bool PaintWindow(LayoutDeviceIntRegion aRegion);
+ bool PaintWindowInDrawTarget(mozilla::gfx::DrawTarget* aDT, const LayoutDeviceIntRegion& aRegion,
+ const mozilla::gfx::IntSize& aSurfaceSize);
+
+ void PaintWindowInContentLayer();
+ void HandleMainThreadCATransaction();
+
+#ifdef ACCESSIBILITY
+ already_AddRefed<mozilla::a11y::LocalAccessible> GetDocumentAccessible();
+#endif
+
+ virtual void CreateCompositor() override;
+
+ virtual bool WidgetPaintsBackground() override { return true; }
+
+ virtual bool PreRender(mozilla::widget::WidgetRenderingContext* aContext) override;
+ virtual void PostRender(mozilla::widget::WidgetRenderingContext* aContext) override;
+ virtual RefPtr<mozilla::layers::NativeLayerRoot> GetNativeLayerRoot() override;
+
+ virtual void UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) override;
+
+ virtual void UpdateWindowDraggingRegion(const LayoutDeviceIntRegion& aRegion) override;
+ LayoutDeviceIntRegion GetNonDraggableRegion() { return mNonDraggableRegion.Region(); }
+
+ virtual void LookUpDictionary(const nsAString& aText,
+ const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical,
+ const LayoutDeviceIntPoint& aPoint) override;
+
+ void ResetParent();
+
+ static bool DoHasPendingInputEvent();
+ static uint32_t GetCurrentInputEventCount();
+ static void UpdateCurrentInputEventCount();
+
+ NSView<mozView>* GetEditorView();
+
+ nsCocoaWindow* GetAppWindowWidget() const;
+
+ virtual void ReparentNativeWidget(nsIWidget* aNewParent) override;
+
+ mozilla::widget::TextInputHandler* GetTextInputHandler() { return mTextInputHandler; }
+
+ // unit conversion convenience functions
+ int32_t CocoaPointsToDevPixels(CGFloat aPts) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aPts, BackingScaleFactor());
+ }
+ LayoutDeviceIntPoint CocoaPointsToDevPixels(const NSPoint& aPt) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aPt, BackingScaleFactor());
+ }
+ LayoutDeviceIntPoint CocoaPointsToDevPixelsRoundDown(const NSPoint& aPt) const {
+ return nsCocoaUtils::CocoaPointsToDevPixelsRoundDown(aPt, BackingScaleFactor());
+ }
+ LayoutDeviceIntRect CocoaPointsToDevPixels(const NSRect& aRect) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aRect, BackingScaleFactor());
+ }
+ CGFloat DevPixelsToCocoaPoints(int32_t aPixels) const {
+ return nsCocoaUtils::DevPixelsToCocoaPoints(aPixels, BackingScaleFactor());
+ }
+ NSRect DevPixelsToCocoaPoints(const LayoutDeviceIntRect& aRect) const {
+ return nsCocoaUtils::DevPixelsToCocoaPoints(aRect, BackingScaleFactor());
+ }
+
+ virtual LayoutDeviceIntPoint GetClientOffset() override;
+
+ void DispatchAPZWheelInputEvent(mozilla::InputData& aEvent);
+ nsEventStatus DispatchAPZInputEvent(mozilla::InputData& aEvent);
+
+ void DispatchDoubleTapGesture(mozilla::TimeStamp aEventTimeStamp,
+ LayoutDeviceIntPoint aScreenPosition,
+ mozilla::Modifiers aModifiers);
+
+ // Called when the main thread enters a phase during which visual changes
+ // are imminent and any layer updates on the compositor thread would interfere
+ // with visual atomicity.
+ // "Async" CATransactions are CATransactions which happen on a thread that's
+ // not the main thread.
+ void SuspendAsyncCATransactions();
+
+ // Called when we know that the current main thread paint will be completed once
+ // the main thread goes back to the event loop.
+ void MaybeScheduleUnsuspendAsyncCATransactions();
+
+ // Called from the runnable dispatched by MaybeScheduleUnsuspendAsyncCATransactions().
+ // At this point we know that the main thread is done handling the visual change
+ // (such as a window resize) and we can start modifying CALayers from the
+ // compositor thread again.
+ void UnsuspendAsyncCATransactions();
+
+ // Called by nsCocoaWindow when the window's fullscreen state changes.
+ void UpdateFullscreen(bool aFullscreen);
+
+#ifdef DEBUG
+ // test only.
+ virtual nsresult SetHiDPIMode(bool aHiDPI) override;
+ virtual nsresult RestoreHiDPIMode() override;
+#endif
+
+ protected:
+ virtual ~nsChildView();
+
+ void ReportMoveEvent();
+ void ReportSizeEvent();
+
+ void TearDownView();
+
+ virtual already_AddRefed<nsIWidget> AllocateChildPopupWidget() override {
+ return nsIWidget::CreateTopLevelWindow();
+ }
+
+ void ConfigureAPZCTreeManager() override;
+ void ConfigureAPZControllerThread() override;
+
+ void UpdateVibrancy(const nsTArray<ThemeGeometry>& aThemeGeometries);
+ mozilla::VibrancyManager& EnsureVibrancyManager();
+
+ nsIWidget* GetWidgetForListenerEvents();
+
+ protected:
+ ChildView* mView; // my parallel cocoa view, [STRONG]
+ RefPtr<mozilla::widget::TextInputHandler> mTextInputHandler;
+ InputContext mInputContext;
+
+ NSView* mParentView;
+ nsIWidget* mParentWidget;
+
+#ifdef ACCESSIBILITY
+ // weak ref to this childview's associated mozAccessible for speed reasons
+ // (we get queried for it *a lot* but don't want to own it)
+ nsWeakPtr mAccessible;
+#endif
+
+ // Held while the compositor (or WR renderer) thread is compositing.
+ // Protects from tearing down the view during compositing and from presenting
+ // half-composited layers to the screen.
+ mozilla::Mutex mCompositingLock MOZ_UNANNOTATED;
+
+ mozilla::ViewRegion mNonDraggableRegion;
+
+ // Cached value of [mView backingScaleFactor], to avoid sending two obj-c
+ // messages (respondsToSelector, backingScaleFactor) every time we need to
+ // use it.
+ // ** We'll need to reinitialize this if the backing resolution changes. **
+ mutable CGFloat mBackingScaleFactor;
+
+ bool mVisible;
+ nsSizeMode mSizeMode;
+ bool mDrawing;
+ bool mIsDispatchPaint; // Is a paint event being dispatched
+
+ RefPtr<mozilla::layers::NativeLayerRootCA> mNativeLayerRoot;
+
+ // In BasicLayers mode, this is the CoreAnimation layer that contains the
+ // rendering from Gecko. It is a sublayer of mNativeLayerRoot's underlying
+ // wrapper layer.
+ // Lazily created by EnsureContentLayerForMainThreadPainting().
+ RefPtr<mozilla::layers::NativeLayerCA> mContentLayer;
+ RefPtr<mozilla::layers::SurfacePoolHandle> mPoolHandle;
+
+ // In BasicLayers mode, this is the invalid region of mContentLayer.
+ LayoutDeviceIntRegion mContentLayerInvalidRegion;
+
+ mozilla::UniquePtr<mozilla::VibrancyManager> mVibrancyManager;
+
+ RefPtr<mozilla::CancelableRunnable> mUnsuspendAsyncCATransactionsRunnable;
+
+ static uint32_t sLastInputEventCount;
+
+ // This is used by SynthesizeNativeTouchPoint to maintain state between
+ // multiple synthesized points
+ mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput;
+};
+
+#endif // nsChildView_h_
diff --git a/widget/cocoa/nsChildView.mm b/widget/cocoa/nsChildView.mm
new file mode 100644
index 0000000000..4f9e69f2b9
--- /dev/null
+++ b/widget/cocoa/nsChildView.mm
@@ -0,0 +1,4926 @@
+/* -*- Mode: objc; tab-width: 2; 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/. */
+
+#include "mozilla/ArrayUtils.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+
+#include <unistd.h>
+#include <math.h>
+
+#include "nsChildView.h"
+#include "nsCocoaWindow.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SwipeTracker.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy
+#include "mozilla/WritingModes.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/SimpleGestureEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+
+#include "nsArrayUtils.h"
+#include "nsExceptionHandler.h"
+#include "nsObjCExceptions.h"
+#include "nsCOMPtr.h"
+#include "nsThreadUtils.h"
+#include "nsToolkit.h"
+#include "nsCRT.h"
+
+#include "nsFontMetrics.h"
+#include "nsIRollupListener.h"
+#include "nsViewManager.h"
+#include "nsIFile.h"
+#include "nsILocalFileMac.h"
+#include "nsGfxCIID.h"
+#include "nsStyleConsts.h"
+#include "nsIWidgetListener.h"
+#include "nsIScreen.h"
+
+#include "nsDragService.h"
+#include "nsClipboard.h"
+#include "nsCursorManager.h"
+#include "nsWindowMap.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "nsMenuUtilsX.h"
+#include "nsMenuBarX.h"
+#include "NativeKeyBindings.h"
+#include "MacThemeGeometryType.h"
+
+#include "gfxContext.h"
+#include "gfxQuartzSurface.h"
+#include "gfxUtils.h"
+#include "nsRegion.h"
+#include "GfxTexturesReporter.h"
+#include "GLTextureImage.h"
+#include "GLContextProvider.h"
+#include "GLContextCGL.h"
+#include "OGLShaderProgram.h"
+#include "ScopedGLHelpers.h"
+#include "HeapCopyOfStackArray.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorOGL.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/NativeLayerCA.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/widget/CompositorWidget.h"
+#include "mozilla/widget/Screen.h"
+#include "gfxUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/BorrowedContext.h"
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+# include "mozilla/a11y/Platform.h"
+#endif
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_ui.h"
+
+#include <dlfcn.h>
+
+#include <ApplicationServices/ApplicationServices.h>
+
+#include "GeckoProfiler.h"
+
+#include "mozilla/layers/ChromeProcessController.h"
+#include "nsLayoutUtils.h"
+#include "InputData.h"
+#include "VibrancyManager.h"
+#include "nsNativeThemeCocoa.h"
+#include "nsIDOMWindowUtils.h"
+#include "Units.h"
+#include "UnitTransforms.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "CustomCocoaEvents.h"
+#include "NativeMenuSupport.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::gl;
+using namespace mozilla::widget;
+
+using mozilla::gfx::Matrix4x4;
+
+#undef DEBUG_UPDATE
+#undef INVALIDATE_DEBUGGING // flash areas as they are invalidated
+
+// Don't put more than this many rects in the dirty region, just fluff
+// out to the bounding-box if there are more
+#define MAX_RECTS_IN_REGION 100
+
+LazyLogModule sCocoaLog("nsCocoaWidgets");
+
+extern "C" {
+CG_EXTERN void CGContextResetCTM(CGContextRef);
+CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform);
+CG_EXTERN void CGContextResetClip(CGContextRef);
+
+typedef CFTypeRef CGSRegionObj;
+CGError CGSNewRegionWithRect(const CGRect* rect, CGSRegionObj* outRegion);
+CGError CGSNewRegionWithRectList(const CGRect* rects, int rectCount, CGSRegionObj* outRegion);
+}
+
+// defined in nsMenuBarX.mm
+extern NSMenu* sApplicationMenu; // Application menu shared by all menubars
+
+extern nsIArray* gDraggedTransferables;
+
+ChildView* ChildViewMouseTracker::sLastMouseEventView = nil;
+NSEvent* ChildViewMouseTracker::sLastMouseMoveEvent = nil;
+NSWindow* ChildViewMouseTracker::sWindowUnderMouse = nil;
+NSPoint ChildViewMouseTracker::sLastScrollEventScreenLocation = NSZeroPoint;
+
+#ifdef INVALIDATE_DEBUGGING
+static void blinkRect(Rect* r);
+static void blinkRgn(RgnHandle rgn);
+#endif
+
+bool gUserCancelledDrag = false;
+
+uint32_t nsChildView::sLastInputEventCount = 0;
+
+static bool sIsTabletPointerActivated = false;
+
+static uint32_t sUniqueKeyEventId = 0;
+
+// The view that will do our drawing or host our NSOpenGLContext or Core Animation layer.
+@interface PixelHostingView : NSView {
+}
+
+@end
+
+@interface ChildView (Private)
+
+// sets up our view, attaching it to its owning gecko view
+- (id)initWithFrame:(NSRect)inFrame geckoChild:(nsChildView*)inChild;
+
+// set up a gecko mouse event based on a cocoa mouse event
+- (void)convertCocoaMouseWheelEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetWheelEvent*)outWheelEvent;
+- (void)convertCocoaMouseEvent:(NSEvent*)aMouseEvent toGeckoEvent:(WidgetInputEvent*)outGeckoEvent;
+- (void)convertCocoaTabletPointerEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetMouseEvent*)outGeckoEvent;
+- (NSMenu*)contextMenu;
+
+- (void)markLayerForDisplay;
+- (CALayer*)rootCALayer;
+- (void)updateRootCALayer;
+
+#ifdef ACCESSIBILITY
+- (id<mozAccessible>)accessible;
+#endif
+
+- (LayoutDeviceIntPoint)convertWindowCoordinates:(NSPoint)aPoint;
+- (LayoutDeviceIntPoint)convertWindowCoordinatesRoundDown:(NSPoint)aPoint;
+
+- (BOOL)inactiveWindowAcceptsMouseEvent:(NSEvent*)aEvent;
+- (void)updateWindowDraggableState;
+
+- (bool)beginOrEndGestureForEventPhase:(NSEvent*)aEvent;
+
+@end
+
+#pragma mark -
+
+// Flips a screen coordinate from a point in the cocoa coordinate system (bottom-left rect) to a
+// point that is a "flipped" cocoa coordinate system (starts in the top-left).
+static inline void FlipCocoaScreenCoordinate(NSPoint& inPoint) {
+ inPoint.y = nsCocoaUtils::FlippedScreenY(inPoint.y);
+}
+
+#pragma mark -
+
+nsChildView::nsChildView()
+ : nsBaseWidget(),
+ mView(nullptr),
+ mParentView(nil),
+ mParentWidget(nullptr),
+ mCompositingLock("ChildViewCompositing"),
+ mBackingScaleFactor(0.0),
+ mVisible(false),
+ mSizeMode(nsSizeMode_Normal),
+ mDrawing(false),
+ mIsDispatchPaint(false) {}
+
+nsChildView::~nsChildView() {
+ // Notify the children that we're gone. childView->ResetParent() can change
+ // our list of children while it's being iterated, so the way we iterate the
+ // list must allow for this.
+ for (nsIWidget* kid = mLastChild; kid;) {
+ nsChildView* childView = static_cast<nsChildView*>(kid);
+ kid = kid->GetPrevSibling();
+ childView->ResetParent();
+ }
+
+ NS_WARNING_ASSERTION(mOnDestroyCalled, "nsChildView object destroyed without calling Destroy()");
+
+ if (mContentLayer) {
+ mNativeLayerRoot->RemoveLayer(mContentLayer); // safe if already removed
+ }
+
+ DestroyCompositor();
+
+ // An nsChildView object that was in use can be destroyed without Destroy()
+ // ever being called on it. So we also need to do a quick, safe cleanup
+ // here (it's too late to just call Destroy(), which can cause crashes).
+ // It's particularly important to make sure widgetDestroyed is called on our
+ // mView -- this method NULLs mView's mGeckoChild, and NULL checks on
+ // mGeckoChild are used throughout the ChildView class to tell if it's safe
+ // to use a ChildView object.
+ [mView widgetDestroyed]; // Safe if mView is nil.
+ mParentWidget = nil;
+ TearDownView(); // Safe if called twice.
+}
+
+nsresult nsChildView::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Because the hidden window is created outside of an event loop,
+ // we need to provide an autorelease pool to avoid leaking cocoa objects
+ // (see bug 559075).
+ nsAutoreleasePool localPool;
+
+ mBounds = aRect;
+
+ // Ensure that the toolkit is created.
+ nsToolkit::GetToolkit();
+
+ BaseCreate(aParent, aInitData);
+
+ mParentView = nil;
+ if (aParent) {
+ // This is the popup window case. aParent is the nsCocoaWindow for the
+ // popup window, and mParentView will be its content view.
+ mParentView = (NSView*)aParent->GetNativeData(NS_NATIVE_WIDGET);
+ mParentWidget = aParent;
+ } else {
+ // This is the top-level window case.
+ // aNativeParent will be the contentView of our window, since that's what
+ // nsCocoaWindow returns when asked for an NS_NATIVE_VIEW.
+ // We do not have a direct "parent widget" association with the top level
+ // window's nsCocoaWindow object.
+ mParentView = reinterpret_cast<NSView*>(aNativeParent);
+ }
+
+ // create our parallel NSView and hook it up to our parent. Recall
+ // that NS_NATIVE_WIDGET is the NSView.
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mParentView);
+ NSRect r = nsCocoaUtils::DevPixelsToCocoaPoints(mBounds, scaleFactor);
+ mView = [[ChildView alloc] initWithFrame:r geckoChild:this];
+
+ mNativeLayerRoot = NativeLayerRootCA::CreateForCALayer([mView rootCALayer]);
+ mNativeLayerRoot->SetBackingScale(scaleFactor);
+
+ // If this view was created in a Gecko view hierarchy, the initial state
+ // is hidden. If the view is attached only to a native NSView but has
+ // no Gecko parent (as in embedding), the initial state is visible.
+ if (mParentWidget)
+ [mView setHidden:YES];
+ else
+ mVisible = true;
+
+ // Hook it up in the NSView hierarchy.
+ if (mParentView) {
+ [mParentView addSubview:mView];
+ }
+
+ // if this is a ChildView, make sure that our per-window data
+ // is set up
+ if ([mView isKindOfClass:[ChildView class]])
+ [[WindowDataMap sharedWindowDataMap] ensureDataForWindow:[mView window]];
+
+ NS_ASSERTION(!mTextInputHandler, "mTextInputHandler has already existed");
+ mTextInputHandler = new TextInputHandler(this, mView);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsChildView::TearDownView() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mView) return;
+
+ NSWindow* win = [mView window];
+ NSResponder* responder = [win firstResponder];
+
+ // We're being unhooked from the view hierarchy, don't leave our view
+ // or a child view as the window first responder.
+ if (responder && [responder isKindOfClass:[NSView class]] &&
+ [(NSView*)responder isDescendantOf:mView]) {
+ [win makeFirstResponder:[mView superview]];
+ }
+
+ // If mView is win's contentView, win (mView's NSWindow) "owns" mView --
+ // win has retained mView, and will detach it from the view hierarchy and
+ // release it when necessary (when win is itself destroyed (in a call to
+ // [win dealloc])). So all we need to do here is call [mView release] (to
+ // match the call to [mView retain] in nsChildView::StandardCreate()).
+ // Also calling [mView removeFromSuperviewWithoutNeedingDisplay] causes
+ // mView to be released again and dealloced, while remaining win's
+ // contentView. So if we do that here, win will (for a short while) have
+ // an invalid contentView (for the consequences see bmo bugs 381087 and
+ // 374260).
+ if ([mView isEqual:[win contentView]]) {
+ [mView release];
+ } else {
+ // Stop NSView hierarchy being changed during [ChildView drawRect:]
+ [mView performSelectorOnMainThread:@selector(delayedTearDown)
+ withObject:nil
+ waitUntilDone:false];
+ }
+ mView = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsCocoaWindow* nsChildView::GetAppWindowWidget() const {
+ id windowDelegate = [[mView window] delegate];
+ if (windowDelegate && [windowDelegate isKindOfClass:[WindowDelegate class]]) {
+ return [(WindowDelegate*)windowDelegate geckoWidget];
+ }
+ return nullptr;
+}
+
+void nsChildView::Destroy() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mOnDestroyCalled) return;
+ mOnDestroyCalled = true;
+
+ // Stuff below may delete the last ref to this
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ {
+ // Make sure that no composition is in progress while disconnecting
+ // ourselves from the view.
+ MutexAutoLock lock(mCompositingLock);
+
+ [mView widgetDestroyed];
+ }
+
+ nsBaseWidget::Destroy();
+
+ NotifyWindowDestroyed();
+ mParentWidget = nil;
+
+ TearDownView();
+
+ nsBaseWidget::OnDestroy();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+#if 0
+static void PrintViewHierarchy(NSView *view)
+{
+ while (view) {
+ NSLog(@" view is %x, frame %@", view, NSStringFromRect([view frame]));
+ view = [view superview];
+ }
+}
+#endif
+
+// Return native data according to aDataType
+void* nsChildView::GetNativeData(uint32_t aDataType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ case NS_NATIVE_WIDGET:
+ retVal = (void*)mView;
+ break;
+
+ case NS_NATIVE_WINDOW:
+ retVal = [mView window];
+ break;
+
+ case NS_NATIVE_GRAPHIC:
+ NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a Mac OS X child view!");
+ retVal = nullptr;
+ break;
+
+ case NS_NATIVE_OFFSETX:
+ retVal = 0;
+ break;
+
+ case NS_NATIVE_OFFSETY:
+ retVal = 0;
+ break;
+
+ case NS_RAW_NATIVE_IME_CONTEXT:
+ retVal = GetPseudoIMEContext();
+ if (retVal) {
+ break;
+ }
+ retVal = [mView inputContext];
+ // If input context isn't available on this widget, we should set |this|
+ // instead of nullptr since if this returns nullptr, IMEStateManager
+ // cannot manage composition with TextComposition instance. Although,
+ // this case shouldn't occur.
+ if (NS_WARN_IF(!retVal)) {
+ retVal = this;
+ }
+ break;
+
+ case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID: {
+ NSWindow* win = [mView window];
+ if (win) {
+ retVal = (void*)[win windowNumber];
+ }
+ break;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+#pragma mark -
+
+void nsChildView::SuppressAnimation(bool aSuppress) {
+ GetAppWindowWidget()->SuppressAnimation(aSuppress);
+}
+
+bool nsChildView::IsVisible() const {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mVisible) {
+ return mVisible;
+ }
+
+ if (!GetAppWindowWidget()->IsVisible()) {
+ return false;
+ }
+
+ // mVisible does not accurately reflect the state of a hidden tabbed view
+ // so verify that the view has a window as well
+ // then check native widget hierarchy visibility
+ return ([mView window] != nil) && !NSIsEmptyRect([mView visibleRect]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+// Some NSView methods (e.g. setFrame and setHidden) invalidate the view's
+// bounds in our window. However, we don't want these invalidations because
+// they are unnecessary and because they actually slow us down since we
+// block on the compositor inside drawRect.
+// When we actually need something invalidated, there will be an explicit call
+// to Invalidate from Gecko, so turning these automatic invalidations off
+// won't hurt us in the non-OMTC case.
+// The invalidations inside these NSView methods happen via a call to the
+// private method -[NSWindow _setNeedsDisplayInRect:]. Our BaseWindow
+// implementation of that method is augmented to let us ignore those calls
+// using -[BaseWindow disable/enableSetNeedsDisplay].
+static void ManipulateViewWithoutNeedingDisplay(NSView* aView, void (^aCallback)()) {
+ BaseWindow* win = nil;
+ if ([[aView window] isKindOfClass:[BaseWindow class]]) {
+ win = (BaseWindow*)[aView window];
+ }
+ [win disableSetNeedsDisplay];
+ aCallback();
+ [win enableSetNeedsDisplay];
+}
+
+// Hide or show this component
+void nsChildView::Show(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aState != mVisible) {
+ // Provide an autorelease pool because this gets called during startup
+ // on the "hidden window", resulting in cocoa object leakage if there's
+ // no pool in place.
+ nsAutoreleasePool localPool;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setHidden:!aState];
+ });
+
+ mVisible = aState;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Change the parent of this widget
+void nsChildView::SetParent(nsIWidget* aNewParent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mOnDestroyCalled) return;
+
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ if (mParentWidget) {
+ mParentWidget->RemoveChild(this);
+ }
+
+ if (aNewParent) {
+ ReparentNativeWidget(aNewParent);
+ } else {
+ [mView removeFromSuperview];
+ mParentView = nil;
+ }
+
+ mParentWidget = aNewParent;
+
+ if (mParentWidget) {
+ mParentWidget->AddChild(this);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::ReparentNativeWidget(nsIWidget* aNewParent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aNewParent, "null widget");
+
+ if (mOnDestroyCalled) return;
+
+ NSView<mozView>* newParentView = (NSView<mozView>*)aNewParent->GetNativeData(NS_NATIVE_WIDGET);
+ NS_ENSURE_TRUE_VOID(newParentView);
+
+ // we hold a ref to mView, so this is safe
+ [mView removeFromSuperview];
+ mParentView = newParentView;
+ [mParentView addSubview:mView];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::ResetParent() {
+ if (!mOnDestroyCalled) {
+ if (mParentWidget) mParentWidget->RemoveChild(this);
+ if (mView) [mView removeFromSuperview];
+ }
+ mParentWidget = nullptr;
+}
+
+nsIWidget* nsChildView::GetParent() { return mParentWidget; }
+
+float nsChildView::GetDPI() {
+ float dpi = 96.0;
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetDpi(&dpi);
+ }
+ return dpi;
+}
+
+void nsChildView::Enable(bool aState) {}
+
+bool nsChildView::IsEnabled() const { return true; }
+
+void nsChildView::SetFocus(Raise, mozilla::dom::CallerType aCallerType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* window = [mView window];
+ if (window) [window makeFirstResponder:mView];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Override to set the cursor on the mac
+void nsChildView::SetCursor(const Cursor& aCursor) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([mView isDragInProgress]) {
+ return; // Don't change the cursor during dragging.
+ }
+
+ nsBaseWidget::SetCursor(aCursor);
+
+ if (NS_SUCCEEDED([[nsCursorManager sharedInstance] setCustomCursor:aCursor
+ widgetScaleFactor:BackingScaleFactor()])) {
+ return;
+ }
+
+ [[nsCursorManager sharedInstance] setNonCustomCursor:aCursor];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// Get this component dimension
+LayoutDeviceIntRect nsChildView::GetBounds() {
+ return !mView ? mBounds : CocoaPointsToDevPixels([mView frame]);
+}
+
+LayoutDeviceIntRect nsChildView::GetClientBounds() {
+ LayoutDeviceIntRect rect = GetBounds();
+ if (!mParentWidget) {
+ // For top level widgets we want the position on screen, not the position
+ // of this view inside the window.
+ rect.MoveTo(WidgetToScreenOffset());
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect nsChildView::GetScreenBounds() {
+ LayoutDeviceIntRect rect = GetBounds();
+ rect.MoveTo(WidgetToScreenOffset());
+ return rect;
+}
+
+double nsChildView::GetDefaultScaleInternal() { return BackingScaleFactor(); }
+
+CGFloat nsChildView::BackingScaleFactor() const {
+ if (mBackingScaleFactor > 0.0) {
+ return mBackingScaleFactor;
+ }
+ if (!mView) {
+ return 1.0;
+ }
+ mBackingScaleFactor = nsCocoaUtils::GetBackingScaleFactor(mView);
+ return mBackingScaleFactor;
+}
+
+void nsChildView::BackingScaleFactorChanged() {
+ CGFloat newScale = nsCocoaUtils::GetBackingScaleFactor(mView);
+
+ // ignore notification if it hasn't really changed (or maybe we have
+ // disabled HiDPI mode via prefs)
+ if (mBackingScaleFactor == newScale) {
+ return;
+ }
+
+ SuspendAsyncCATransactions();
+ mBackingScaleFactor = newScale;
+ NSRect frame = [mView frame];
+ mBounds = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, newScale);
+
+ mNativeLayerRoot->SetBackingScale(mBackingScaleFactor);
+
+ if (mWidgetListener && !mWidgetListener->GetAppWindow()) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ }
+}
+
+int32_t nsChildView::RoundsWidgetCoordinatesTo() {
+ if (BackingScaleFactor() == 2.0) {
+ return 2;
+ }
+ return 1;
+}
+
+// Move this component, aX and aY are in the parent widget coordinate system
+void nsChildView::Move(double aX, double aY) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t x = NSToIntRound(aX);
+ int32_t y = NSToIntRound(aY);
+
+ if (!mView || (mBounds.x == x && mBounds.y == y)) return;
+
+ mBounds.x = x;
+ mBounds.y = y;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ ReportMoveEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::Resize(double aWidth, double aHeight, bool aRepaint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+
+ if (!mView || (mBounds.width == width && mBounds.height == height)) return;
+
+ SuspendAsyncCATransactions();
+ mBounds.width = width;
+ mBounds.height = height;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ if (mVisible && aRepaint) {
+ [[mView pixelHostingView] setNeedsDisplay:YES];
+ }
+
+ ReportSizeEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::Resize(double aX, double aY, double aWidth, double aHeight, bool aRepaint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t x = NSToIntRound(aX);
+ int32_t y = NSToIntRound(aY);
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+
+ BOOL isMoving = (mBounds.x != x || mBounds.y != y);
+ BOOL isResizing = (mBounds.width != width || mBounds.height != height);
+ if (!mView || (!isMoving && !isResizing)) return;
+
+ if (isMoving) {
+ mBounds.x = x;
+ mBounds.y = y;
+ }
+ if (isResizing) {
+ SuspendAsyncCATransactions();
+ mBounds.width = width;
+ mBounds.height = height;
+
+ CALayer* layer = [mView rootCALayer];
+ double scale = BackingScaleFactor();
+ layer.bounds = CGRectMake(0, 0, width / scale, height / scale);
+ }
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ if (mVisible && aRepaint) {
+ [[mView pixelHostingView] setNeedsDisplay:YES];
+ }
+
+ if (isMoving) {
+ ReportMoveEvent();
+ if (mOnDestroyCalled) return;
+ }
+ if (isResizing) ReportSizeEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// The following three methods are primarily an attempt to avoid glitches during
+// window resizing.
+// Here's some background on how these glitches come to be:
+// CoreAnimation transactions are per-thread. They don't nest across threads.
+// If you submit a transaction on the main thread and a transaction on a
+// different thread, the two will race to the window server and show up on the
+// screen in the order that they happen to arrive in at the window server.
+// When the window size changes, there's another event that needs to be
+// synchronized with: the window "shape" change. Cocoa has built-in synchronization
+// mechanics that make sure that *main thread* window paints during window resizes
+// are synchronized properly with the window shape change. But no such built-in
+// synchronization exists for CATransactions that are triggered on a non-main
+// thread.
+// To cope with this, we define a "danger zone" during which we simply avoid
+// triggering any CATransactions on a non-main thread (called "async" CATransactions
+// here). This danger zone starts at the earliest opportunity at which we know
+// about the size change, which is nsChildView::Resize, and ends at a point at
+// which we know for sure that the paint has been handled completely, which is
+// when we return to the event loop after layer display.
+void nsChildView::SuspendAsyncCATransactions() {
+ if (mUnsuspendAsyncCATransactionsRunnable) {
+ mUnsuspendAsyncCATransactionsRunnable->Cancel();
+ mUnsuspendAsyncCATransactionsRunnable = nullptr;
+ }
+
+ // Make sure that there actually will be a CATransaction on the main thread
+ // during which we get a chance to schedule unsuspension. Otherwise we might
+ // accidentally stay suspended indefinitely.
+ [mView markLayerForDisplay];
+
+ mNativeLayerRoot->SuspendOffMainThreadCommits();
+}
+
+void nsChildView::MaybeScheduleUnsuspendAsyncCATransactions() {
+ if (mNativeLayerRoot->AreOffMainThreadCommitsSuspended() &&
+ !mUnsuspendAsyncCATransactionsRunnable) {
+ mUnsuspendAsyncCATransactionsRunnable =
+ NewCancelableRunnableMethod("nsChildView::MaybeScheduleUnsuspendAsyncCATransactions", this,
+ &nsChildView::UnsuspendAsyncCATransactions);
+ NS_DispatchToMainThread(mUnsuspendAsyncCATransactionsRunnable);
+ }
+}
+
+void nsChildView::UnsuspendAsyncCATransactions() {
+ mUnsuspendAsyncCATransactionsRunnable = nullptr;
+
+ if (mNativeLayerRoot->UnsuspendOffMainThreadCommits()) {
+ // We need to call mNativeLayerRoot->CommitToScreen() at the next available
+ // opportunity.
+ // The easiest way to handle this request is to mark the layer as needing
+ // display, because this will schedule a main thread CATransaction, during
+ // which HandleMainThreadCATransaction will call CommitToScreen().
+ [mView markLayerForDisplay];
+ }
+}
+
+void nsChildView::UpdateFullscreen(bool aFullscreen) {
+ if (mNativeLayerRoot) {
+ mNativeLayerRoot->SetWindowIsFullscreen(aFullscreen);
+ }
+}
+
+nsresult nsChildView::SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode, uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "keyevent");
+ return mTextInputHandler->SynthesizeNativeKeyEvent(
+ aNativeKeyboardLayout, aNativeKeyCode, aModifierFlags, aCharacters, aUnmodifiedCharacters);
+}
+
+nsresult nsChildView::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+
+ NSPoint pt = nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+
+ // Move the mouse cursor to the requested position and reconnect it to the mouse.
+ CGWarpMouseCursorPosition(NSPointToCGPoint(pt));
+ CGAssociateMouseAndMouseCursorPosition(true);
+
+ // aPoint is given with the origin on the top left, but convertScreenToBase
+ // expects a point in a coordinate system that has its origin on the bottom left.
+ NSPoint screenPoint = NSMakePoint(pt.x, nsCocoaUtils::FlippedScreenY(pt.y));
+ NSPoint windowPoint = nsCocoaUtils::ConvertPointFromScreen([mView window], screenPoint);
+ NSEventModifierFlags modifierFlags =
+ nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(aModifierFlags);
+
+ if (aButton == MouseButton::eX1 || aButton == MouseButton::eX2) {
+ // NSEvent has `buttonNumber` for `NSEventTypeOther*`. However, it seems that
+ // there is no way to specify it. Therefore, we should return error for now.
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NSEventType nativeEventType;
+ switch (aNativeMessage) {
+ case NativeMouseMessage::ButtonDown:
+ case NativeMouseMessage::ButtonUp: {
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeLeftMouseDown
+ : NSEventTypeLeftMouseUp;
+ break;
+ case MouseButton::eMiddle:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeOtherMouseDown
+ : NSEventTypeOtherMouseUp;
+ break;
+ case MouseButton::eSecondary:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeRightMouseDown
+ : NSEventTypeRightMouseUp;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ }
+ case NativeMouseMessage::Move:
+ nativeEventType = NSEventTypeMouseMoved;
+ break;
+ case NativeMouseMessage::EnterWindow:
+ nativeEventType = NSEventTypeMouseEntered;
+ break;
+ case NativeMouseMessage::LeaveWindow:
+ nativeEventType = NSEventTypeMouseExited;
+ break;
+ }
+
+ NSEvent* event = [NSEvent mouseEventWithType:nativeEventType
+ location:windowPoint
+ modifierFlags:modifierFlags
+ timestamp:[[NSProcessInfo processInfo] systemUptime]
+ windowNumber:[[mView window] windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:0.0];
+
+ if (!event) return NS_ERROR_FAILURE;
+
+ if ([[mView window] isKindOfClass:[BaseWindow class]]) {
+ // Tracking area events don't end up in their tracking areas when sent
+ // through [NSApp sendEvent:], so pass them directly to the right methods.
+ BaseWindow* window = (BaseWindow*)[mView window];
+ if (nativeEventType == NSEventTypeMouseEntered) {
+ [window mouseEntered:event];
+ return NS_OK;
+ }
+ if (nativeEventType == NSEventTypeMouseExited) {
+ [window mouseExited:event];
+ return NS_OK;
+ }
+ if (nativeEventType == NSEventTypeMouseMoved) {
+ [window mouseMoved:event];
+ return NS_OK;
+ }
+ }
+
+ [NSApp sendEvent:event];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, double aDeltaY,
+ double aDeltaZ, uint32_t aModifierFlags, uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+
+ NSPoint pt = nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+
+ // Move the mouse cursor to the requested position and reconnect it to the mouse.
+ CGWarpMouseCursorPosition(NSPointToCGPoint(pt));
+ CGAssociateMouseAndMouseCursorPosition(true);
+
+ // Mostly copied from http://stackoverflow.com/a/6130349
+ CGScrollEventUnit units = (aAdditionalFlags & nsIDOMWindowUtils::MOUSESCROLL_SCROLL_LINES)
+ ? kCGScrollEventUnitLine
+ : kCGScrollEventUnitPixel;
+ CGEventRef cgEvent = CGEventCreateScrollWheelEvent(NULL, units, 3, (int32_t)aDeltaY,
+ (int32_t)aDeltaX, (int32_t)aDeltaZ);
+ if (!cgEvent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aNativeMessage) {
+ CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventScrollPhase, aNativeMessage);
+ }
+
+ // On macOS 10.14 and up CGEventPost won't work because of changes in macOS
+ // to improve security. This code makes an NSEvent corresponding to the
+ // wheel event and dispatches it directly to the scrollWheel handler. Some
+ // fiddling is needed with the coordinates in order to simulate what macOS
+ // would do; this code adapted from the Chromium equivalent function at
+ // https://chromium.googlesource.com/chromium/src.git/+/62.0.3178.1/ui/events/test/cocoa_test_event_utils.mm#38
+ CGPoint location = CGEventGetLocation(cgEvent);
+ location.y += NSMinY([[mView window] frame]);
+ location.x -= NSMinX([[mView window] frame]);
+ CGEventSetLocation(cgEvent, location);
+
+ uint64_t kNanosPerSec = 1000000000L;
+ CGEventSetTimestamp(cgEvent, [[NSProcessInfo processInfo] systemUptime] * kNanosPerSec);
+
+ NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
+ [event setValue:[mView window] forKey:@"_window"];
+ [mView scrollWheel:event];
+
+ CFRelease(cgEvent);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, TouchPointerState aPointerState, mozilla::LayoutDeviceIntPoint aPoint,
+ double aPointerPressure, uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aPointerState == TOUCH_HOVER) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mSynthesizedTouchInput) {
+ mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
+ }
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
+ mSynthesizedTouchInput.get(), TimeStamp::Now(), aPointerId, aPointerState, pointInWindow,
+ aPointerPressure, aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeTouchpadDoubleTap(mozilla::LayoutDeviceIntPoint aPoint,
+ uint32_t aModifierFlags) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ DispatchDoubleTapGesture(TimeStamp::Now(), aPoint - WidgetToScreenOffset(),
+ static_cast<Modifiers>(aModifierFlags));
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsChildView::SendEventToNativeMenuSystem(NSEvent* aEvent) {
+ bool handled = false;
+ nsCocoaWindow* widget = GetAppWindowWidget();
+ if (widget) {
+ nsMenuBarX* mb = widget->GetMenuBar();
+ if (mb) {
+ // Check if main menu wants to handle the event.
+ handled = mb->PerformKeyEquivalent(aEvent);
+ }
+ }
+
+ if (!handled && sApplicationMenu) {
+ // Check if application menu wants to handle the event.
+ handled = [sApplicationMenu performKeyEquivalent:aEvent];
+ }
+
+ return handled;
+}
+
+void nsChildView::PostHandleKeyEvent(mozilla::WidgetKeyboardEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // We always allow keyboard events to propagate to keyDown: but if they are
+ // not handled we give menu items a chance to act. This allows for handling of
+ // custom shortcuts. Note that existing shortcuts cannot be reassigned yet and
+ // will have been handled by keyDown: before we get here.
+ NSMutableDictionary* nativeKeyEventsMap = [ChildView sNativeKeyEventsMap];
+ NSEvent* cocoaEvent = [nativeKeyEventsMap objectForKey:@(aEvent->mUniqueId)];
+ if (!cocoaEvent) {
+ return;
+ }
+
+ // If the escape key is pressed, the expectations are as follows:
+ // 1. If the page is loading, interrupt loading.
+ // 2. Give a website an opportunity to handle the event and call
+ // preventDefault() on it.
+ // 3. If the browser is fullscreen and the page isn't loading, exit
+ // fullscreen.
+ // 4. Ignore.
+ // Case 1 and 2 are handled before we get here. Below, we handle case 3.
+ if (StaticPrefs::browser_fullscreen_exit_on_escape() && [cocoaEvent keyCode] == kVK_Escape &&
+ [[mView window] styleMask] & NSWindowStyleMaskFullScreen) {
+ [[mView window] toggleFullScreen:nil];
+ }
+
+ if (SendEventToNativeMenuSystem(cocoaEvent)) {
+ aEvent->PreventDefault();
+ }
+ [nativeKeyEventsMap removeObjectForKey:@(aEvent->mUniqueId)];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Used for testing native menu system structure and event handling.
+nsresult nsChildView::ActivateNativeMenuItemAt(const nsAString& indexString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsMenuUtilsX::CheckNativeMenuConsistency([NSApp mainMenu]);
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading())
+ length:indexString.Length()];
+ NSMenuItem* item =
+ nsMenuUtilsX::NativeMenuItemWithLocation([NSApp mainMenu], locationString, true);
+ // We can't perform an action on an item with a submenu, that will raise
+ // an obj-c exception.
+ if (item && ![item hasSubmenu]) {
+ NSMenu* parent = [item menu];
+ if (parent) {
+ // NSLog(@"Performing action for native menu item titled: %@\n",
+ // [[currentSubmenu itemAtIndex:targetIndex] title]);
+ mozilla::AutoRestore<bool> autoRestore(
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
+ [parent performActionForItemAtIndex:[parent indexOfItem:item]];
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// Used for testing native menu system structure and event handling.
+nsresult nsChildView::ForceUpdateNativeMenuAt(const nsAString& indexString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCocoaWindow* widget = GetAppWindowWidget();
+ if (widget) {
+ nsMenuBarX* mb = widget->GetMenuBar();
+ if (mb) {
+ if (indexString.IsEmpty())
+ mb->ForceNativeMenuReload();
+ else
+ mb->ForceUpdateNativeMenuAt(indexString);
+ }
+ }
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+#pragma mark -
+
+#ifdef INVALIDATE_DEBUGGING
+
+static Boolean KeyDown(const UInt8 theKey) {
+ KeyMap map;
+ GetKeys(map);
+ return ((*((UInt8*)map + (theKey >> 3)) >> (theKey & 7)) & 1) != 0;
+}
+
+static Boolean caps_lock() { return KeyDown(0x39); }
+
+static void blinkRect(Rect* r) {
+ StRegionFromPool oldClip;
+ if (oldClip != NULL) ::GetClip(oldClip);
+
+ ::ClipRect(r);
+ ::InvertRect(r);
+ UInt32 end = ::TickCount() + 5;
+ while (::TickCount() < end)
+ ;
+ ::InvertRect(r);
+
+ if (oldClip != NULL) ::SetClip(oldClip);
+}
+
+static void blinkRgn(RgnHandle rgn) {
+ StRegionFromPool oldClip;
+ if (oldClip != NULL) ::GetClip(oldClip);
+
+ ::SetClip(rgn);
+ ::InvertRgn(rgn);
+ UInt32 end = ::TickCount() + 5;
+ while (::TickCount() < end)
+ ;
+ ::InvertRgn(rgn);
+
+ if (oldClip != NULL) ::SetClip(oldClip);
+}
+
+#endif
+
+// Invalidate this component's visible area
+void nsChildView::Invalidate(const LayoutDeviceIntRect& aRect) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mView || !mVisible) return;
+
+ NS_ASSERTION(GetWindowRenderer()->GetBackendType() != LayersBackend::LAYERS_WR,
+ "Shouldn't need to invalidate with accelerated OMTC layers!");
+
+ EnsureContentLayerForMainThreadPainting();
+ mContentLayerInvalidRegion.OrWith(aRect.Intersect(GetBounds()));
+ [mView markLayerForDisplay];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool nsChildView::WidgetTypeSupportsAcceleration() {
+ // All widget types support acceleration.
+ return true;
+}
+
+bool nsChildView::ShouldUseOffMainThreadCompositing() {
+ // We need to enable OMTC in popups which contain remote layer
+ // trees, since the remote content won't be rendered at all otherwise.
+ if (HasRemoteContent()) {
+ return true;
+ }
+
+ // Don't use OMTC for popup windows, because we do not want context menus to
+ // pay the overhead of starting up a compositor. With the OpenGL compositor,
+ // new windows are expensive because of shader re-compilation, and with
+ // WebRender, new windows are expensive because they create their own threads
+ // and texture caches.
+ // Using OMTC with BasicCompositor for context menus would probably be fine
+ // but isn't a well-tested configuration.
+ if ([mView window] && [[mView window] isKindOfClass:[PopupWindow class]]) {
+ // Use main-thread BasicLayerManager for drawing menus.
+ return false;
+ }
+
+ return nsBaseWidget::ShouldUseOffMainThreadCompositing();
+}
+
+#pragma mark -
+
+// Invokes callback and ProcessEvent methods on Event Listener object
+nsresult nsChildView::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus) {
+ RefPtr<nsChildView> kungFuDeathGrip(this);
+
+#ifdef DEBUG
+ debug_DumpEvent(stdout, event->mWidget, event, "something", 0);
+#endif
+
+ if (event->mFlags.mIsSynthesizedForTests) {
+ WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
+ if (keyEvent) {
+ nsresult rv = mTextInputHandler->AttachNativeKeyEvent(*keyEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ aStatus = nsEventStatus_eIgnore;
+
+ nsIWidgetListener* listener = mWidgetListener;
+
+ // If the listener is NULL, check if the parent is a popup. If it is, then
+ // this child is the popup content view attached to a popup. Get the
+ // listener from the parent popup instead.
+ nsCOMPtr<nsIWidget> parentWidget = mParentWidget;
+ if (!listener && parentWidget) {
+ if (parentWidget->GetWindowType() == WindowType::Popup) {
+ // Check just in case event->mWidget isn't this widget
+ if (event->mWidget) {
+ listener = event->mWidget->GetWidgetListener();
+ }
+ if (!listener) {
+ event->mWidget = parentWidget;
+ listener = parentWidget->GetWidgetListener();
+ }
+ }
+ }
+
+ if (listener) aStatus = listener->HandleEvent(event, mUseAttachedEvents);
+
+ return NS_OK;
+}
+
+nsIWidget* nsChildView::GetWidgetForListenerEvents() {
+ // If there is no listener, use the parent popup's listener if that exists.
+ if (!mWidgetListener && mParentWidget && mParentWidget->GetWindowType() == WindowType::Popup) {
+ return mParentWidget;
+ }
+
+ return this;
+}
+
+void nsChildView::WillPaintWindow() {
+ nsCOMPtr<nsIWidget> widget = GetWidgetForListenerEvents();
+
+ nsIWidgetListener* listener = widget->GetWidgetListener();
+ if (listener) {
+ listener->WillPaintWindow(widget);
+ }
+}
+
+bool nsChildView::PaintWindow(LayoutDeviceIntRegion aRegion) {
+ nsCOMPtr<nsIWidget> widget = GetWidgetForListenerEvents();
+
+ nsIWidgetListener* listener = widget->GetWidgetListener();
+ if (!listener) return false;
+
+ bool returnValue = false;
+ bool oldDispatchPaint = mIsDispatchPaint;
+ mIsDispatchPaint = true;
+ returnValue = listener->PaintWindow(widget, aRegion);
+
+ listener = widget->GetWidgetListener();
+ if (listener) {
+ listener->DidPaintWindow();
+ }
+
+ mIsDispatchPaint = oldDispatchPaint;
+ return returnValue;
+}
+
+bool nsChildView::PaintWindowInDrawTarget(gfx::DrawTarget* aDT,
+ const LayoutDeviceIntRegion& aRegion,
+ const gfx::IntSize& aSurfaceSize) {
+ if (!aDT || !aDT->IsValid()) {
+ return false;
+ }
+ gfxContext targetContext(aDT);
+
+ // Set up the clip region and clear existing contents in the backing surface.
+ targetContext.NewPath();
+ for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) {
+ const LayoutDeviceIntRect& r = iter.Get();
+ targetContext.Rectangle(gfxRect(r.x, r.y, r.width, r.height));
+ aDT->ClearRect(gfx::Rect(r.ToUnknownRect()));
+ }
+ targetContext.Clip();
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(mView);
+ if (GetWindowRenderer()->GetBackendType() == LayersBackend::LAYERS_NONE) {
+ nsBaseWidget::AutoLayerManagerSetup setupLayerManager(this, &targetContext,
+ BufferMode::BUFFER_NONE);
+ return PaintWindow(aRegion);
+ }
+ return false;
+}
+
+void nsChildView::EnsureContentLayerForMainThreadPainting() {
+ // Ensure we have an mContentLayer of the correct size.
+ // The content layer gets created on demand for BasicLayers windows. We do
+ // not create it during widget creation because, for non-BasicLayers windows,
+ // the compositing layer manager will create any layers it needs.
+ gfx::IntSize size = GetBounds().Size().ToUnknownSize();
+ if (mContentLayer && mContentLayer->GetSize() != size) {
+ mNativeLayerRoot->RemoveLayer(mContentLayer);
+ mContentLayer = nullptr;
+ }
+ if (!mContentLayer) {
+ mPoolHandle = SurfacePool::Create(0)->GetHandleForGL(nullptr);
+ RefPtr<NativeLayer> contentLayer = mNativeLayerRoot->CreateLayer(size, false, mPoolHandle);
+ mNativeLayerRoot->AppendLayer(contentLayer);
+ mContentLayer = contentLayer->AsNativeLayerCA();
+ mContentLayer->SetSurfaceIsFlipped(false);
+ mContentLayerInvalidRegion = GetBounds();
+ }
+}
+
+void nsChildView::PaintWindowInContentLayer() {
+ EnsureContentLayerForMainThreadPainting();
+ mPoolHandle->OnBeginFrame();
+ RefPtr<DrawTarget> dt = mContentLayer->NextSurfaceAsDrawTarget(
+ gfx::IntRect({}, mContentLayer->GetSize()), mContentLayerInvalidRegion.ToUnknownRegion(),
+ gfx::BackendType::SKIA);
+ if (!dt) {
+ return;
+ }
+
+ PaintWindowInDrawTarget(dt, mContentLayerInvalidRegion, dt->GetSize());
+ mContentLayer->NotifySurfaceReady();
+ mContentLayerInvalidRegion.SetEmpty();
+ mPoolHandle->OnEndFrame();
+}
+
+void nsChildView::HandleMainThreadCATransaction() {
+ WillPaintWindow();
+
+ if (GetWindowRenderer()->GetBackendType() == LayersBackend::LAYERS_NONE) {
+ // We're in BasicLayers mode, i.e. main thread software compositing.
+ // Composite the window into our layer's surface.
+ PaintWindowInContentLayer();
+ } else {
+ // Trigger a synchronous OMTC composite. This will call NextSurface and
+ // NotifySurfaceReady on the compositor thread to update mNativeLayerRoot's
+ // contents, and the main thread (this thread) will wait inside PaintWindow
+ // during that time.
+ PaintWindow(LayoutDeviceIntRegion(GetBounds()));
+ }
+
+ {
+ // Apply the changes inside mNativeLayerRoot to the underlying CALayers. Now is a
+ // good time to call this because we know we're currently inside a main thread
+ // CATransaction, and the lock makes sure that no composition is currently in
+ // progress, so we won't present half-composited state to the screen.
+ MutexAutoLock lock(mCompositingLock);
+ mNativeLayerRoot->CommitToScreen();
+ }
+
+ MaybeScheduleUnsuspendAsyncCATransactions();
+}
+
+#pragma mark -
+
+void nsChildView::ReportMoveEvent() { NotifyWindowMoved(mBounds.x, mBounds.y); }
+
+void nsChildView::ReportSizeEvent() {
+ if (mWidgetListener) mWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+}
+
+#pragma mark -
+
+LayoutDeviceIntPoint nsChildView::GetClientOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSPoint origin = [mView convertPoint:NSMakePoint(0, 0) toView:nil];
+ origin.y = [[mView window] frame].size.height - origin.y;
+ return CocoaPointsToDevPixels(origin);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+// Return the offset between this child view and the screen.
+// @return -- widget origin in device-pixel coords
+LayoutDeviceIntPoint nsChildView::WidgetToScreenOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSPoint origin = NSMakePoint(0, 0);
+
+ // 1. First translate view origin point into window coords.
+ // The returned point is in bottom-left coordinates.
+ origin = [mView convertPoint:origin toView:nil];
+
+ // 2. We turn the window-coord rect's origin into screen (still bottom-left) coords.
+ origin = nsCocoaUtils::ConvertPointToScreen([mView window], origin);
+
+ // 3. Since we're dealing in bottom-left coords, we need to make it top-left coords
+ // before we pass it back to Gecko.
+ FlipCocoaScreenCoordinate(origin);
+
+ // convert to device pixels
+ return CocoaPointsToDevPixels(origin);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+nsresult nsChildView::SetTitle(const nsAString& title) {
+ // child views don't have titles
+ return NS_OK;
+}
+
+nsresult nsChildView::GetAttention(int32_t aCycleCount) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [NSApp requestUserAttention:NSInformationalRequest];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+/* static */
+bool nsChildView::DoHasPendingInputEvent() {
+ return sLastInputEventCount != GetCurrentInputEventCount();
+}
+
+/* static */
+uint32_t nsChildView::GetCurrentInputEventCount() {
+ // Can't use kCGAnyInputEventType because that updates too rarely for us (and
+ // always in increments of 30+!) and because apparently it's sort of broken
+ // on Tiger. So just go ahead and query the counters we care about.
+ static const CGEventType eventTypes[] = {
+ kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown,
+ kCGEventRightMouseUp, kCGEventMouseMoved, kCGEventLeftMouseDragged,
+ kCGEventRightMouseDragged, kCGEventKeyDown, kCGEventKeyUp,
+ kCGEventScrollWheel, kCGEventTabletPointer, kCGEventOtherMouseDown,
+ kCGEventOtherMouseUp, kCGEventOtherMouseDragged};
+
+ uint32_t eventCount = 0;
+ for (uint32_t i = 0; i < ArrayLength(eventTypes); ++i) {
+ eventCount +=
+ CGEventSourceCounterForEventType(kCGEventSourceStateCombinedSessionState, eventTypes[i]);
+ }
+ return eventCount;
+}
+
+/* static */
+void nsChildView::UpdateCurrentInputEventCount() {
+ sLastInputEventCount = GetCurrentInputEventCount();
+}
+
+bool nsChildView::HasPendingInputEvent() { return DoHasPendingInputEvent(); }
+
+#pragma mark -
+
+void nsChildView::SetInputContext(const InputContext& aContext, const InputContextAction& aAction) {
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+
+ if (mTextInputHandler->IsFocused()) {
+ if (aContext.IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+ }
+
+ // IMEInputHandler::IsEditableContent() returns false when both
+ // IsASCIICableOnly() and IsIMEEnabled() return false. So, be careful
+ // when you change the following code. You might need to change
+ // IMEInputHandler::IsEditableContent() too.
+ mInputContext = aContext;
+ switch (aContext.mIMEState.mEnabled) {
+ case IMEEnabled::Enabled:
+ mTextInputHandler->SetASCIICapableOnly(false);
+ mTextInputHandler->EnableIME(true);
+ if (mInputContext.mIMEState.mOpen != IMEState::DONT_CHANGE_OPEN_STATE) {
+ mTextInputHandler->SetIMEOpenState(mInputContext.mIMEState.mOpen == IMEState::OPEN);
+ }
+ break;
+ case IMEEnabled::Disabled:
+ mTextInputHandler->SetASCIICapableOnly(false);
+ mTextInputHandler->EnableIME(false);
+ break;
+ case IMEEnabled::Password:
+ mTextInputHandler->SetASCIICapableOnly(true);
+ mTextInputHandler->EnableIME(false);
+ break;
+ default:
+ NS_ERROR("not implemented!");
+ }
+}
+
+InputContext nsChildView::GetInputContext() {
+ switch (mInputContext.mIMEState.mEnabled) {
+ case IMEEnabled::Enabled:
+ if (mTextInputHandler) {
+ mInputContext.mIMEState.mOpen =
+ mTextInputHandler->IsIMEOpened() ? IMEState::OPEN : IMEState::CLOSED;
+ break;
+ }
+ // If mTextInputHandler is null, set CLOSED instead...
+ [[fallthrough]];
+ default:
+ mInputContext.mIMEState.mOpen = IMEState::CLOSED;
+ break;
+ }
+ return mInputContext;
+}
+
+TextEventDispatcherListener* nsChildView::GetNativeTextEventDispatcherListener() {
+ if (NS_WARN_IF(!mTextInputHandler)) {
+ return nullptr;
+ }
+ return mTextInputHandler;
+}
+
+nsresult nsChildView::AttachNativeKeyEvent(mozilla::WidgetKeyboardEvent& aEvent) {
+ NS_ENSURE_TRUE(mTextInputHandler, NS_ERROR_NOT_AVAILABLE);
+ return mTextInputHandler->AttachNativeKeyEvent(aEvent);
+}
+
+bool nsChildView::GetEditCommands(NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ Maybe<WritingMode> writingMode;
+ if (aEvent.NeedsToRemapNavigationKey()) {
+ if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+ writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ }
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
+ return true;
+}
+
+NSView<mozView>* nsChildView::GetEditorView() {
+ NSView<mozView>* editorView = mView;
+ // We need to get editor's view. E.g., when the focus is in the bookmark
+ // dialog, the view is <panel> element of the dialog. At this time, the key
+ // events are processed the parent window's view that has native focus.
+ WidgetQueryContentEvent queryContentState(true, eQueryContentState, this);
+ // This may be called during creating a menu popup frame due to creating
+ // widget synchronously and that causes Cocoa asking current window level.
+ // In this case, it's not safe to flush layout on the document and we don't
+ // need any layout information right now.
+ queryContentState.mNeedsToFlushLayout = false;
+ DispatchWindowEvent(queryContentState);
+ if (queryContentState.Succeeded() && queryContentState.mReply->mFocusedWidget) {
+ NSView<mozView>* view = static_cast<NSView<mozView>*>(
+ queryContentState.mReply->mFocusedWidget->GetNativeData(NS_NATIVE_WIDGET));
+ if (view) editorView = view;
+ }
+ return editorView;
+}
+
+#pragma mark -
+
+void nsChildView::CreateCompositor() {
+ nsBaseWidget::CreateCompositor();
+ if (mCompositorBridgeChild) {
+ [mView setUsingOMTCompositor:true];
+ }
+}
+
+void nsChildView::ConfigureAPZCTreeManager() { nsBaseWidget::ConfigureAPZCTreeManager(); }
+
+void nsChildView::ConfigureAPZControllerThread() { nsBaseWidget::ConfigureAPZControllerThread(); }
+
+bool nsChildView::PreRender(WidgetRenderingContext* aContext) MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ // The lock makes sure that we don't attempt to tear down the view while
+ // compositing. That would make us unable to call postRender on it when the
+ // composition is done, thus keeping the GL context locked forever.
+ mCompositingLock.Lock();
+
+ if (aContext->mGL && gfxPlatform::CanMigrateMacGPUs()) {
+ GLContextCGL::Cast(aContext->mGL)->MigrateToActiveGPU();
+ }
+
+ return true;
+}
+
+void nsChildView::PostRender(WidgetRenderingContext* aContext) MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ mCompositingLock.Unlock();
+}
+
+RefPtr<layers::NativeLayerRoot> nsChildView::GetNativeLayerRoot() { return mNativeLayerRoot; }
+
+static int32_t FindTitlebarBottom(const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ int32_t aWindowWidth) {
+ int32_t titlebarBottom = 0;
+ for (auto& g : aThemeGeometries) {
+ if (g.mType == eThemeGeometryTypeTitlebar && g.mRect.X() <= 0 &&
+ g.mRect.XMost() >= aWindowWidth && g.mRect.Y() <= 0) {
+ titlebarBottom = std::max(titlebarBottom, g.mRect.YMost());
+ }
+ }
+ return titlebarBottom;
+}
+
+static int32_t FindUnifiedToolbarBottom(const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ int32_t aWindowWidth, int32_t aTitlebarBottom) {
+ int32_t unifiedToolbarBottom = aTitlebarBottom;
+ for (uint32_t i = 0; i < aThemeGeometries.Length(); ++i) {
+ const nsIWidget::ThemeGeometry& g = aThemeGeometries[i];
+ if ((g.mType == eThemeGeometryTypeToolbar) && g.mRect.X() <= 0 &&
+ g.mRect.XMost() >= aWindowWidth && g.mRect.Y() <= aTitlebarBottom) {
+ unifiedToolbarBottom = std::max(unifiedToolbarBottom, g.mRect.YMost());
+ }
+ }
+ return unifiedToolbarBottom;
+}
+
+static LayoutDeviceIntRect FindFirstRectOfType(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ nsITheme::ThemeGeometryType aThemeGeometryType) {
+ for (uint32_t i = 0; i < aThemeGeometries.Length(); ++i) {
+ const nsIWidget::ThemeGeometry& g = aThemeGeometries[i];
+ if (g.mType == aThemeGeometryType) {
+ return g.mRect;
+ }
+ }
+ return LayoutDeviceIntRect();
+}
+
+void nsChildView::UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ if (![mView window]) return;
+
+ UpdateVibrancy(aThemeGeometries);
+
+ if (![[mView window] isKindOfClass:[ToolbarWindow class]]) return;
+
+ // Update unified toolbar height and sheet attachment position.
+ int32_t windowWidth = mBounds.width;
+ int32_t titlebarBottom = FindTitlebarBottom(aThemeGeometries, windowWidth);
+ int32_t unifiedToolbarBottom =
+ FindUnifiedToolbarBottom(aThemeGeometries, windowWidth, titlebarBottom);
+ int32_t toolboxBottom = FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeToolbox).YMost();
+
+ ToolbarWindow* win = (ToolbarWindow*)[mView window];
+ int32_t titlebarHeight =
+ [win drawsContentsIntoWindowFrame] ? 0 : CocoaPointsToDevPixels([win titlebarHeight]);
+ int32_t devUnifiedHeight = titlebarHeight + unifiedToolbarBottom;
+ [win setUnifiedToolbarHeight:DevPixelsToCocoaPoints(devUnifiedHeight)];
+
+ int32_t sheetPositionDevPx = std::max(toolboxBottom, unifiedToolbarBottom);
+ NSPoint sheetPositionView = {0, DevPixelsToCocoaPoints(sheetPositionDevPx)};
+ NSPoint sheetPositionWindow = [mView convertPoint:sheetPositionView toView:nil];
+ [win setSheetAttachmentPosition:sheetPositionWindow.y];
+
+ // Update titlebar control offsets.
+ LayoutDeviceIntRect windowButtonRect =
+ FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeWindowButtons);
+ [win placeWindowButtons:[mView convertRect:DevPixelsToCocoaPoints(windowButtonRect) toView:nil]];
+}
+
+static Maybe<VibrancyType> ThemeGeometryTypeToVibrancyType(
+ nsITheme::ThemeGeometryType aThemeGeometryType) {
+ switch (aThemeGeometryType) {
+ case eThemeGeometryTypeTooltip:
+ return Some(VibrancyType::TOOLTIP);
+ case eThemeGeometryTypeMenu:
+ return Some(VibrancyType::MENU);
+ case eThemeGeometryTypeHighlightedMenuItem:
+ return Some(VibrancyType::HIGHLIGHTED_MENUITEM);
+ case eThemeGeometryTypeSourceList:
+ return Some(VibrancyType::SOURCE_LIST);
+ case eThemeGeometryTypeSourceListSelection:
+ return Some(VibrancyType::SOURCE_LIST_SELECTION);
+ case eThemeGeometryTypeActiveSourceListSelection:
+ return Some(VibrancyType::ACTIVE_SOURCE_LIST_SELECTION);
+ default:
+ return Nothing();
+ }
+}
+
+static LayoutDeviceIntRegion GatherVibrantRegion(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries, VibrancyType aVibrancyType) {
+ LayoutDeviceIntRegion region;
+ for (auto& geometry : aThemeGeometries) {
+ if (ThemeGeometryTypeToVibrancyType(geometry.mType) == Some(aVibrancyType)) {
+ region.OrWith(geometry.mRect);
+ }
+ }
+ return region;
+}
+
+template <typename Region>
+static void MakeRegionsNonOverlappingImpl(Region& aOutUnion) {}
+
+template <typename Region, typename... Regions>
+static void MakeRegionsNonOverlappingImpl(Region& aOutUnion, Region& aFirst, Regions&... aRest) {
+ MakeRegionsNonOverlappingImpl(aOutUnion, aRest...);
+ aFirst.SubOut(aOutUnion);
+ aOutUnion.OrWith(aFirst);
+}
+
+// Subtracts parts from regions in such a way that they don't have any overlap.
+// Each region in the argument list will have the union of all the regions
+// *following* it subtracted from itself. In other words, the arguments are
+// sorted low priority to high priority.
+template <typename Region, typename... Regions>
+static void MakeRegionsNonOverlapping(Region& aFirst, Regions&... aRest) {
+ Region unionOfAll;
+ MakeRegionsNonOverlappingImpl(unionOfAll, aFirst, aRest...);
+}
+
+void nsChildView::UpdateVibrancy(const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ LayoutDeviceIntRegion menuRegion = GatherVibrantRegion(aThemeGeometries, VibrancyType::MENU);
+ LayoutDeviceIntRegion tooltipRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::TOOLTIP);
+ LayoutDeviceIntRegion highlightedMenuItemRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::HIGHLIGHTED_MENUITEM);
+ LayoutDeviceIntRegion sourceListRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::SOURCE_LIST);
+ LayoutDeviceIntRegion sourceListSelectionRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::SOURCE_LIST_SELECTION);
+ LayoutDeviceIntRegion activeSourceListSelectionRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::ACTIVE_SOURCE_LIST_SELECTION);
+
+ MakeRegionsNonOverlapping(menuRegion, tooltipRegion, highlightedMenuItemRegion, sourceListRegion,
+ sourceListSelectionRegion, activeSourceListSelectionRegion);
+
+ auto& vm = EnsureVibrancyManager();
+ bool changed = false;
+ changed |= vm.UpdateVibrantRegion(VibrancyType::MENU, menuRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::TOOLTIP, tooltipRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::HIGHLIGHTED_MENUITEM, highlightedMenuItemRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::SOURCE_LIST, sourceListRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::SOURCE_LIST_SELECTION, sourceListSelectionRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::ACTIVE_SOURCE_LIST_SELECTION,
+ activeSourceListSelectionRegion);
+
+ if (changed) {
+ SuspendAsyncCATransactions();
+ }
+}
+
+mozilla::VibrancyManager& nsChildView::EnsureVibrancyManager() {
+ MOZ_ASSERT(mView, "Only call this once we have a view!");
+ if (!mVibrancyManager) {
+ mVibrancyManager = MakeUnique<VibrancyManager>(*this, [mView vibrancyViewsContainer]);
+ }
+ return *mVibrancyManager;
+}
+
+void nsChildView::UpdateBoundsFromView() {
+ auto oldSize = mBounds.Size();
+ mBounds = CocoaPointsToDevPixels([mView frame]);
+ if (mBounds.Size() != oldSize) {
+ SuspendAsyncCATransactions();
+ }
+}
+
+@interface NonDraggableView : NSView
+@end
+
+@implementation NonDraggableView
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil;
+}
+- (NSRect)_opaqueRectForWindowMoveWhenInTitlebar {
+ // In NSWindows that use NSWindowStyleMaskFullSizeContentView, NSViews which
+ // overlap the titlebar do not disable window dragging in the overlapping
+ // areas even if they return NO from mouseDownCanMoveWindow. This can have
+ // unfortunate effects: For example, dragging tabs in a browser window would
+ // move the window if those tabs are in the titlebar.
+ // macOS does not seem to offer a documented way to opt-out of the forced
+ // window dragging in the titlebar.
+ // Overriding _opaqueRectForWindowMoveWhenInTitlebar is an undocumented way
+ // of opting out of this behavior. This method was added in 10.11 and is used
+ // by some NSControl subclasses to prevent window dragging in the titlebar.
+ // The function which assembles the draggable area of the window calls
+ // _opaqueRect for the content area and _opaqueRectForWindowMoveWhenInTitlebar
+ // for the titlebar area, on all visible NSViews. The default implementation
+ // of _opaqueRect returns [self visibleRect], and the default implementation
+ // of _opaqueRectForWindowMoveWhenInTitlebar returns NSZeroRect unless it's
+ // overridden.
+ return [self visibleRect];
+}
+@end
+
+void nsChildView::UpdateWindowDraggingRegion(const LayoutDeviceIntRegion& aRegion) {
+ // mView returns YES from mouseDownCanMoveWindow, so we need to put NSViews
+ // that return NO from mouseDownCanMoveWindow in the places that shouldn't
+ // be draggable. We can't do it the other way round because returning
+ // YES from mouseDownCanMoveWindow doesn't have any effect if there's a
+ // superview that returns NO.
+ LayoutDeviceIntRegion nonDraggable;
+ nonDraggable.Sub(LayoutDeviceIntRect(0, 0, mBounds.width, mBounds.height), aRegion);
+
+ __block bool changed = false;
+
+ // Suppress calls to setNeedsDisplay during NSView geometry changes.
+ ManipulateViewWithoutNeedingDisplay(mView, ^() {
+ changed = mNonDraggableRegion.UpdateRegion(
+ nonDraggable, *this, [mView nonDraggableViewsContainer], ^() {
+ return [[NonDraggableView alloc] initWithFrame:NSZeroRect];
+ });
+ });
+
+ if (changed) {
+ // Trigger an update to the window server. This will call
+ // mouseDownCanMoveWindow.
+ // Doing this manually is only necessary because we're suppressing
+ // setNeedsDisplay calls above.
+ [[mView window] setMovableByWindowBackground:NO];
+ [[mView window] setMovableByWindowBackground:YES];
+ }
+}
+
+nsEventStatus nsChildView::DispatchAPZInputEvent(InputData& aEvent) {
+ APZEventResult result;
+
+ if (mAPZC) {
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent);
+ }
+
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return result.GetStatus();
+ }
+
+ if (aEvent.mInputType == PINCHGESTURE_INPUT) {
+ PinchGestureInput& pinchEvent = aEvent.AsPinchGestureInput();
+ WidgetWheelEvent wheelEvent = pinchEvent.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&wheelEvent, result);
+ } else if (aEvent.mInputType == TAPGESTURE_INPUT) {
+ TapGestureInput& tapEvent = aEvent.AsTapGestureInput();
+ WidgetSimpleGestureEvent gestureEvent = tapEvent.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&gestureEvent, result);
+ } else {
+ MOZ_ASSERT_UNREACHABLE();
+ }
+
+ return result.GetStatus();
+}
+
+void nsChildView::DispatchAPZWheelInputEvent(InputData& aEvent) {
+ if (mSwipeTracker && aEvent.mInputType == PANGESTURE_INPUT) {
+ // Give the swipe tracker a first pass at the event. If a new pan gesture
+ // has been started since the beginning of the swipe, the swipe tracker
+ // will know to ignore the event.
+ nsEventStatus status = mSwipeTracker->ProcessEvent(aEvent.AsPanGestureInput());
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+
+ WidgetWheelEvent event(true, eWheel, this);
+
+ if (mAPZC) {
+ APZEventResult result;
+
+ switch (aEvent.mInputType) {
+ case PANGESTURE_INPUT: {
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+
+ event = MayStartSwipeForAPZ(aEvent.AsPanGestureInput(), result);
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ // For wheel events on OS X, send it to APZ using the WidgetInputEvent
+ // variant of ReceiveInputEvent, because the APZInputBridge version of
+ // that function has special handling (for delta multipliers etc.) that
+ // we need to run. Using the InputData variant would bypass that and
+ // go straight to the APZCTreeManager subclass.
+ event = aEvent.AsScrollWheelInput().ToWidgetEvent(this);
+ result = mAPZC->InputBridge()->ReceiveInputEvent(event);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ break;
+ };
+ default:
+ MOZ_CRASH("unsupported event type");
+ return;
+ }
+ if (event.mMessage == eWheel && (event.mDeltaX != 0 || event.mDeltaY != 0)) {
+ ProcessUntransformedAPZEvent(&event, result);
+ }
+ return;
+ }
+
+ nsEventStatus status;
+ switch (aEvent.mInputType) {
+ case PANGESTURE_INPUT: {
+ if (MayStartSwipeForNonAPZ(aEvent.AsPanGestureInput())) {
+ return;
+ }
+ event = aEvent.AsPanGestureInput().ToWidgetEvent(this);
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ event = aEvent.AsScrollWheelInput().ToWidgetEvent(this);
+ break;
+ }
+ default:
+ MOZ_CRASH("unexpected event type");
+ return;
+ }
+ if (event.mMessage == eWheel && (event.mDeltaX != 0 || event.mDeltaY != 0)) {
+ DispatchEvent(&event, status);
+ }
+}
+
+void nsChildView::DispatchDoubleTapGesture(TimeStamp aEventTimeStamp,
+ LayoutDeviceIntPoint aScreenPosition,
+ mozilla::Modifiers aModifiers) {
+ if (StaticPrefs::apz_mac_enable_double_tap_zoom_touchpad_gesture()) {
+ TapGestureInput event{
+ TapGestureInput::TAPGESTURE_DOUBLE, aEventTimeStamp,
+ ViewAs<ScreenPixel>(aScreenPosition,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent),
+ aModifiers};
+
+ DispatchAPZInputEvent(event);
+ } else {
+ // Setup the "double tap" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eTapGesture, this);
+ // do what convertCocoaMouseEvent does basically.
+ geckoEvent.mRefPoint = aScreenPosition;
+ geckoEvent.mModifiers = aModifiers;
+ geckoEvent.mTimeStamp = aEventTimeStamp;
+ geckoEvent.mClickCount = 1;
+
+ // Send the event.
+ DispatchWindowEvent(geckoEvent);
+ }
+}
+
+void nsChildView::LookUpDictionary(const nsAString& aText,
+ const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSMutableAttributedString* attrStr = nsCocoaUtils::GetNSMutableAttributedString(
+ aText, aFontRangeArray, aIsVertical, BackingScaleFactor());
+ NSPoint pt = nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+ NSDictionary* attributes = [attrStr attributesAtIndex:0 effectiveRange:nil];
+ NSFont* font = [attributes objectForKey:NSFontAttributeName];
+ if (font) {
+ if (aIsVertical) {
+ pt.x -= [font descender];
+ } else {
+ pt.y += [font ascender];
+ }
+ }
+
+ [mView showDefinitionForAttributedString:attrStr atPoint:pt];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#ifdef ACCESSIBILITY
+already_AddRefed<a11y::LocalAccessible> nsChildView::GetDocumentAccessible() {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return nullptr;
+
+ // mAccessible might be dead if accessibility was previously disabled and is
+ // now being enabled again.
+ if (mAccessible && mAccessible->IsAlive()) {
+ RefPtr<a11y::LocalAccessible> ret;
+ CallQueryReferent(mAccessible.get(), static_cast<a11y::LocalAccessible**>(getter_AddRefs(ret)));
+ return ret.forget();
+ }
+
+ // need to fetch the accessible anew, because it has gone away.
+ // cache the accessible in our weak ptr
+ RefPtr<a11y::LocalAccessible> acc = GetRootAccessible();
+ mAccessible = do_GetWeakReference(acc.get());
+
+ return acc.forget();
+}
+#endif
+
+class WidgetsReleaserRunnable final : public mozilla::Runnable {
+ public:
+ explicit WidgetsReleaserRunnable(nsTArray<nsCOMPtr<nsIWidget>>&& aWidgetArray)
+ : mozilla::Runnable("WidgetsReleaserRunnable"), mWidgetArray(std::move(aWidgetArray)) {}
+
+ // Do nothing; all this runnable does is hold a reference the widgets in
+ // mWidgetArray, and those references will be dropped when this runnable
+ // is destroyed.
+
+ private:
+ nsTArray<nsCOMPtr<nsIWidget>> mWidgetArray;
+};
+
+#pragma mark -
+
+// ViewRegionContainerView is a view class for certain subviews of ChildView
+// which contain the NSViews created for ViewRegions (see ViewRegion.h).
+// It doesn't do anything interesting, it only acts as a container so that it's
+// easier for ChildView to control the z order of its children.
+@interface ViewRegionContainerView : NSView {
+}
+@end
+
+@implementation ViewRegionContainerView
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil; // Be transparent to mouse events.
+}
+
+- (BOOL)isFlipped {
+ return [[self superview] isFlipped];
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ return [[self superview] mouseDownCanMoveWindow];
+}
+
+@end
+
+@implementation ChildView
+
+// globalDragPboard is non-null during native drag sessions that did not originate
+// in our native NSView (it is set in |draggingEntered:|). It is unset when the
+// drag session ends for this view, either with the mouse exiting or when a drop
+// occurs in this view.
+NSPasteboard* globalDragPboard = nil;
+
+// gLastDragView and gLastDragMouseDownEvent are used to communicate information
+// to the drag service during drag invocation (starting a drag in from the view).
+// gLastDragView is only non-null while a mouse button is pressed, so between
+// mouseDown and mouseUp.
+NSView* gLastDragView = nil; // [weak]
+NSEvent* gLastDragMouseDownEvent = nil; // [strong]
+
++ (void)initialize {
+ static BOOL initialized = NO;
+
+ if (!initialized) {
+ // Inform the OS about the types of services (from the "Services" menu)
+ // that we can handle.
+ NSArray* types = @[
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeHTML]
+ ];
+ [NSApp registerServicesMenuSendTypes:types returnTypes:types];
+ initialized = YES;
+ }
+}
+
++ (void)registerViewForDraggedTypes:(NSView*)aView {
+ [aView
+ registerForDraggedTypes:
+ [NSArray
+ arrayWithObjects:[UTIHelper stringFromPboardType:NSFilenamesPboardType],
+ [UTIHelper stringFromPboardType:kMozFileUrlsPboardType],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeHTML],
+ [UTIHelper
+ stringFromPboardType:(NSString*)kPasteboardTypeFileURLPromise],
+ [UTIHelper stringFromPboardType:kMozWildcardPboardType],
+ [UTIHelper stringFromPboardType:kPublicUrlPboardType],
+ [UTIHelper stringFromPboardType:kPublicUrlNamePboardType],
+ [UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType], nil]];
+}
+
+// initWithFrame:geckoChild:
+- (id)initWithFrame:(NSRect)inFrame geckoChild:(nsChildView*)inChild {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super initWithFrame:inFrame])) {
+ mGeckoChild = inChild;
+ mBlockedLastMouseDown = NO;
+ mExpectingWheelStop = NO;
+
+ mLastMouseDownEvent = nil;
+ mLastKeyDownEvent = nil;
+ mClickThroughMouseDownEvent = nil;
+ mDragService = nullptr;
+
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+
+ mIsUpdatingLayer = NO;
+
+ [self setFocusRingType:NSFocusRingTypeNone];
+
+#ifdef __LP64__
+ mCancelSwipeAnimation = nil;
+#endif
+
+ mNonDraggableViewsContainer = [[ViewRegionContainerView alloc] initWithFrame:[self bounds]];
+ mVibrancyViewsContainer = [[ViewRegionContainerView alloc] initWithFrame:[self bounds]];
+
+ [mNonDraggableViewsContainer setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ [mVibrancyViewsContainer setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+ [self addSubview:mNonDraggableViewsContainer];
+ [self addSubview:mVibrancyViewsContainer];
+
+ mPixelHostingView = [[PixelHostingView alloc] initWithFrame:[self bounds]];
+ [mPixelHostingView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+ [self addSubview:mPixelHostingView];
+
+ mRootCALayer = [[CALayer layer] retain];
+ mRootCALayer.position = NSZeroPoint;
+ mRootCALayer.bounds = NSZeroRect;
+ mRootCALayer.anchorPoint = NSZeroPoint;
+ mRootCALayer.contentsGravity = kCAGravityTopLeft;
+ [[mPixelHostingView layer] addSublayer:mRootCALayer];
+
+ mLastPressureStage = 0;
+ }
+
+ // register for things we'll take from other applications
+ [ChildView registerViewForDraggedTypes:self];
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSTextInputContext*)inputContext {
+ if (!mGeckoChild) {
+ // -[ChildView widgetDestroyed] has been called, but
+ // -[ChildView delayedTearDown] has not yet completed. Accessing
+ // [super inputContext] now would uselessly recreate a text input context
+ // for us, under which -[ChildView validAttributesForMarkedText] would
+ // be called and the assertion checking for mTextInputHandler would fail.
+ // We return nil to avoid that.
+ return nil;
+ }
+ return [super inputContext];
+}
+
+- (void)installTextInputHandler:(TextInputHandler*)aHandler {
+ mTextInputHandler = aHandler;
+}
+
+- (void)uninstallTextInputHandler {
+ mTextInputHandler = nullptr;
+}
+
+- (NSView*)vibrancyViewsContainer {
+ return mVibrancyViewsContainer;
+}
+
+- (NSView*)nonDraggableViewsContainer {
+ return mNonDraggableViewsContainer;
+}
+
+- (NSView*)pixelHostingView {
+ return mPixelHostingView;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mLastMouseDownEvent release];
+ [mLastKeyDownEvent release];
+ [mClickThroughMouseDownEvent release];
+ ChildViewMouseTracker::OnDestroyView(self);
+
+ [mVibrancyViewsContainer removeFromSuperview];
+ [mVibrancyViewsContainer release];
+ [mNonDraggableViewsContainer removeFromSuperview];
+ [mNonDraggableViewsContainer release];
+ [mPixelHostingView removeFromSuperview];
+ [mPixelHostingView release];
+ [mRootCALayer release];
+
+ if (gLastDragView == self) {
+ gLastDragView = nil;
+ }
+
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)widgetDestroyed {
+ if (mTextInputHandler) {
+ mTextInputHandler->OnDestroyWidget(mGeckoChild);
+ mTextInputHandler = nullptr;
+ }
+ mGeckoChild = nullptr;
+
+ // Just in case we're destroyed abruptly and missed the draggingExited
+ // or performDragOperation message.
+ NS_IF_RELEASE(mDragService);
+}
+
+// mozView method, return our gecko child view widget. Note this does not AddRef.
+- (nsIWidget*)widget {
+ return static_cast<nsIWidget*>(mGeckoChild);
+}
+
+- (NSString*)description {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSString stringWithFormat:@"ChildView %p, gecko child %p, frame %@", self, mGeckoChild,
+ NSStringFromRect([self frame])];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Make the origin of this view the topLeft corner (gecko origin) rather
+// than the bottomLeft corner (standard cocoa origin).
+- (BOOL)isFlipped {
+ return YES;
+}
+
+// We accept key and mouse events, so don't keep passing them up the chain. Allow
+// this to be a 'focused' widget for event dispatch.
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+// Accept mouse down events on background windows
+- (BOOL)acceptsFirstMouse:(NSEvent*)aEvent {
+ if (![[self window] isKindOfClass:[PopupWindow class]]) {
+ // We rely on this function to tell us that the mousedown was on a
+ // background window. Inside mouseDown we can't tell whether we were
+ // inactive because at that point we've already been made active.
+ // Unfortunately, acceptsFirstMouse is called for PopupWindows even when
+ // their parent window is active, so ignore this on them for now.
+ mClickThroughMouseDownEvent = [aEvent retain];
+ }
+ return YES;
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ // Return YES so that parts of this view can be draggable. The non-draggable
+ // parts will be covered by NSViews that return NO from
+ // mouseDownCanMoveWindow and thus override draggability from the inside.
+ // These views are assembled in nsChildView::UpdateWindowDraggingRegion.
+ return YES;
+}
+
+- (void)viewDidChangeBackingProperties {
+ [super viewDidChangeBackingProperties];
+ if (mGeckoChild) {
+ // actually, it could be the color space that's changed,
+ // but we can't tell the difference here except by retrieving
+ // the backing scale factor and comparing to the old value
+ mGeckoChild->BackingScaleFactorChanged();
+ }
+}
+
+- (BOOL)isCoveringTitlebar {
+ return [[self window] isKindOfClass:[BaseWindow class]] &&
+ [(BaseWindow*)[self window] mainChildView] == self &&
+ [(BaseWindow*)[self window] drawsContentsIntoWindowFrame];
+}
+
+- (void)viewWillStartLiveResize {
+ nsCocoaWindow* windowWidget = mGeckoChild ? mGeckoChild->GetAppWindowWidget() : nullptr;
+ if (windowWidget) {
+ windowWidget->NotifyLiveResizeStarted();
+ }
+}
+
+- (void)viewDidEndLiveResize {
+ // mGeckoChild may legitimately be null here. It should also have been null
+ // in viewWillStartLiveResize, so there's no problem. However if we run into
+ // cases where the windowWidget was non-null in viewWillStartLiveResize but
+ // is null here, that might be problematic because we might get stuck with
+ // a content process that has the displayport suppressed. If that scenario
+ // arises (I'm not sure that it does) we will need to handle it gracefully.
+ nsCocoaWindow* windowWidget = mGeckoChild ? mGeckoChild->GetAppWindowWidget() : nullptr;
+ if (windowWidget) {
+ windowWidget->NotifyLiveResizeStopped();
+ }
+}
+
+- (void)markLayerForDisplay {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ if (!mIsUpdatingLayer) {
+ // This call will cause updateRootCALayer to be called during the upcoming
+ // main thread CoreAnimation transaction. It will also trigger a transaction
+ // if no transaction is currently pending.
+ [[mPixelHostingView layer] setNeedsDisplay];
+ }
+}
+
+- (void)ensureNextCompositeIsAtomicWithMainThreadPaint {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ if (mGeckoChild) {
+ mGeckoChild->SuspendAsyncCATransactions();
+ }
+}
+
+- (void)updateRootCALayer {
+ if (NS_IsMainThread() && mGeckoChild) {
+ MOZ_RELEASE_ASSERT(!mIsUpdatingLayer, "Re-entrant layer display?");
+ mIsUpdatingLayer = YES;
+ mGeckoChild->HandleMainThreadCATransaction();
+ mIsUpdatingLayer = NO;
+ }
+}
+
+- (CALayer*)rootCALayer {
+ return mRootCALayer;
+}
+
+// If we've just created a non-native context menu, we need to mark it as
+// such and let the OS (and other programs) know when it opens and closes
+// (this is how the OS knows to close other programs' context menus when
+// ours open). We send the initial notification here, but others are sent
+// in nsCocoaWindow::Show().
+- (void)maybeInitContextMenuTracking {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus()) {
+ return;
+ }
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE_VOID(rollupListener);
+ nsCOMPtr<nsIWidget> widget = rollupListener->GetRollupWidget();
+ NS_ENSURE_TRUE_VOID(widget);
+
+ NSWindow* popupWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!popupWindow || ![popupWindow isKindOfClass:[PopupWindow class]]) return;
+
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.HIToolbox.beginMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ [(PopupWindow*)popupWindow setIsContextMenu:YES];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Returns true if the event should no longer be processed, false otherwise.
+// This does not return whether or not anything was rolled up.
+- (BOOL)maybeRollup:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ BOOL consumeEvent = NO;
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE(rollupListener, false);
+
+ BOOL isWheelTypeEvent = [theEvent type] == NSEventTypeScrollWheel ||
+ [theEvent type] == NSEventTypeMagnify ||
+ [theEvent type] == NSEventTypeSmartMagnify;
+
+ if (!isWheelTypeEvent && rollupListener->RollupNativeMenu()) {
+ // A native menu was rolled up.
+ // Don't consume this event; if the menu wanted to consume this event it would already have done
+ // so and we wouldn't even get here. For example, we won't get here for left clicks that close
+ // native menus (because the native menu consumes it), but we will get here for right clicks
+ // that close native menus, and we do not want to consume those right clicks.
+ return NO;
+ }
+
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return consumeEvent;
+ }
+
+ NSWindow* currentPopup = static_cast<NSWindow*>(rollupWidget->GetNativeData(NS_NATIVE_WINDOW));
+ if (nsCocoaUtils::IsEventOverWindow(theEvent, currentPopup)) {
+ return consumeEvent;
+ }
+
+ // Check to see if scroll/zoom events should roll up the popup
+ if (isWheelTypeEvent) {
+ // consume scroll events that aren't over the popup unless the popup is an
+ // arrow panel.
+ consumeEvent = rollupListener->ShouldConsumeOnMouseWheelEvent();
+ if (!rollupListener->ShouldRollupOnMouseWheelEvent()) {
+ return consumeEvent;
+ }
+ }
+
+ // if we're dealing with menus, we probably have submenus and
+ // we don't want to rollup if the click is in a parent menu of
+ // the current submenu
+ uint32_t popupsToRollup = UINT32_MAX;
+ AutoTArray<nsIWidget*, 5> widgetChain;
+ uint32_t sameTypeCount = rollupListener->GetSubmenuWidgetChain(&widgetChain);
+ for (uint32_t i = 0; i < widgetChain.Length(); i++) {
+ nsIWidget* widget = widgetChain[i];
+ NSWindow* currWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (nsCocoaUtils::IsEventOverWindow(theEvent, currWindow)) {
+ // don't roll up if the mouse event occurred within a menu of the
+ // same type. If the mouse event occurred in a menu higher than
+ // that, roll up, but pass the number of popups to Rollup so
+ // that only those of the same type close up.
+ if (i < sameTypeCount) {
+ return consumeEvent;
+ }
+ popupsToRollup = sameTypeCount;
+ break;
+ }
+ }
+
+ LayoutDeviceIntPoint devPoint;
+ nsIRollupListener::RollupOptions rollupOptions{popupsToRollup,
+ nsIRollupListener::FlushViews::Yes};
+ if ([theEvent type] == NSEventTypeLeftMouseDown) {
+ NSPoint point = [NSEvent mouseLocation];
+ FlipCocoaScreenCoordinate(point);
+ devPoint = mGeckoChild->CocoaPointsToDevPixels(point);
+ rollupOptions.mPoint = &devPoint;
+ }
+ consumeEvent = (BOOL)rollupListener->Rollup(rollupOptions);
+ return consumeEvent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)swipeWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ float deltaX = [anEvent deltaX]; // left=1.0, right=-1.0
+ float deltaY = [anEvent deltaY]; // up=1.0, down=-1.0
+
+ // Setup the "swipe" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eSwipeGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+
+ // Record the left/right direction.
+ if (deltaX > 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_LEFT;
+ else if (deltaX < 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT;
+
+ // Record the up/down direction.
+ if (deltaY > 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_UP;
+ else if (deltaY < 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_DOWN;
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Pinch zoom gesture.
+- (void)magnifyWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self maybeRollup:anEvent]) {
+ return;
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ // Instead of calling beginOrEndGestureForEventPhase we basically inline
+ // the effects of it here, because that function doesn't play too well with
+ // how we create PinchGestureInput events below. The main point of that
+ // function is to avoid flip-flopping between rotation/magnify gestures, which
+ // we can do by checking and setting mGestureState appropriately. A secondary
+ // result of that function is to send the final eMagnifyGesture event when
+ // the gesture ends, but APZ takes care of that for us.
+ if (mGestureState == eGestureState_RotateGesture && [anEvent phase] != NSEventPhaseBegan) {
+ // If we're already in a rotation and not "starting" a magnify, abort.
+ return;
+ }
+ mGestureState = eGestureState_MagnifyGesture;
+
+ NSPoint locationInWindow = nsCocoaUtils::EventLocationForWindow(anEvent, [self window]);
+ ScreenPoint position =
+ ViewAs<ScreenPixel>([self convertWindowCoordinatesRoundDown:locationInWindow],
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ ExternalPoint screenOffset =
+ ViewAs<ExternalPixel>(mGeckoChild->WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ TimeStamp eventTimeStamp = nsCocoaUtils::GetEventTimeStamp([anEvent timestamp]);
+ NSEventPhase eventPhase = [anEvent phase];
+ PinchGestureInput::PinchGestureType pinchGestureType;
+
+ switch (eventPhase) {
+ case NSEventPhaseBegan: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ break;
+ }
+ case NSEventPhaseChanged: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ break;
+ }
+ case NSEventPhaseEnded: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ mGestureState = eGestureState_None;
+ break;
+ }
+ default: {
+ NS_WARNING("Unexpected phase for pinch gesture event.");
+ return;
+ }
+ }
+
+ PinchGestureInput event{pinchGestureType,
+ PinchGestureInput::TRACKPAD,
+ eventTimeStamp,
+ screenOffset,
+ position,
+ 100.0,
+ 100.0 * (1.0 - [anEvent magnification]),
+ nsCocoaUtils::ModifiersForEvent(anEvent)};
+
+ mGeckoChild->DispatchAPZInputEvent(event);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Smart zoom gesture, i.e. two-finger double tap on trackpads.
+- (void)smartMagnifyWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild || [self beginOrEndGestureForEventPhase:anEvent]) {
+ return;
+ }
+
+ if ([self maybeRollup:anEvent]) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if (StaticPrefs::apz_mac_enable_double_tap_zoom_touchpad_gesture()) {
+ TimeStamp eventTimeStamp = nsCocoaUtils::GetEventTimeStamp([anEvent timestamp]);
+ NSPoint locationInWindow = nsCocoaUtils::EventLocationForWindow(anEvent, [self window]);
+ LayoutDevicePoint position = [self convertWindowCoordinatesRoundDown:locationInWindow];
+
+ mGeckoChild->DispatchDoubleTapGesture(eventTimeStamp, RoundedToInt(position),
+ nsCocoaUtils::ModifiersForEvent(anEvent));
+ } else {
+ // Setup the "double tap" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eTapGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mClickCount = 1;
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+ }
+
+ // Clear the gesture state
+ mGestureState = eGestureState_None;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rotateWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild || [self beginOrEndGestureForEventPhase:anEvent]) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ float rotation = [anEvent rotation];
+
+ EventMessage msg;
+ switch (mGestureState) {
+ case eGestureState_StartGesture:
+ msg = eRotateGestureStart;
+ mGestureState = eGestureState_RotateGesture;
+ break;
+
+ case eGestureState_RotateGesture:
+ msg = eRotateGestureUpdate;
+ break;
+
+ case eGestureState_None:
+ case eGestureState_MagnifyGesture:
+ default:
+ return;
+ }
+
+ // Setup the event.
+ WidgetSimpleGestureEvent geckoEvent(true, msg, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mDelta = -rotation;
+ if (rotation > 0.0) {
+ geckoEvent.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE;
+ } else {
+ geckoEvent.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE;
+ }
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+
+ // Keep track of the cumulative rotation for the final "rotate" event.
+ mCumulativeRotation += rotation;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// `beginGestureWithEvent` and `endGestureWithEvent` are not called for
+// applications that link against the macOS 10.11 or later SDK when we're
+// running on macOS 10.11 or later. For compatibility with all supported macOS
+// versions, we have to call {begin,end}GestureWithEvent ourselves based on
+// the event phase when we're handling gestures.
+- (bool)beginOrEndGestureForEventPhase:(NSEvent*)aEvent {
+ if (!aEvent) {
+ return false;
+ }
+
+ if (aEvent.phase == NSEventPhaseBegan) {
+ [self beginGestureWithEvent:aEvent];
+ return true;
+ }
+
+ if (aEvent.phase == NSEventPhaseEnded || aEvent.phase == NSEventPhaseCancelled) {
+ [self endGestureWithEvent:aEvent];
+ return true;
+ }
+
+ return false;
+}
+
+- (void)beginGestureWithEvent:(NSEvent*)aEvent {
+ if (!aEvent) {
+ return;
+ }
+
+ mGestureState = eGestureState_StartGesture;
+ mCumulativeRotation = 0.0;
+}
+
+- (void)endGestureWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild) {
+ // Clear the gestures state if we cannot send an event.
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ switch (mGestureState) {
+ case eGestureState_RotateGesture: {
+ // Setup the "rotate" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eRotateGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mDelta = -mCumulativeRotation;
+ if (mCumulativeRotation > 0.0) {
+ geckoEvent.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE;
+ } else {
+ geckoEvent.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE;
+ }
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+ } break;
+
+ case eGestureState_MagnifyGesture: // APZ handles sending the widget events
+ case eGestureState_None:
+ case eGestureState_StartGesture:
+ default:
+ break;
+ }
+
+ // Clear the gestures state.
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)setUsingOMTCompositor:(BOOL)aUseOMTC {
+ mUsingOMTCompositor = aUseOMTC;
+}
+
+// Returning NO from this method only disallows ordering on mousedown - in order
+// to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering]
+// when handling the mousedown event.
+- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)aEvent {
+ // Always using system-provided window ordering for normal windows.
+ if (![[self window] isKindOfClass:[PopupWindow class]]) return NO;
+
+ // Don't reorder when we don't have a parent window, like when we're a
+ // context menu or a tooltip.
+ return ![[self window] parentWindow];
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self shouldDelayWindowOrderingForEvent:theEvent]) {
+ [NSApp preventWindowOrdering];
+ }
+
+ // If we've already seen this event due to direct dispatch from menuForEvent:
+ // just bail; if not, remember it.
+ if (mLastMouseDownEvent == theEvent) {
+ [mLastMouseDownEvent release];
+ mLastMouseDownEvent = nil;
+ return;
+ } else {
+ [mLastMouseDownEvent release];
+ mLastMouseDownEvent = [theEvent retain];
+ }
+
+ [gLastDragMouseDownEvent release];
+ gLastDragMouseDownEvent = [theEvent retain];
+ gLastDragView = self;
+
+ // We need isClickThrough because at this point the window we're in might
+ // already have become main, so the check for isMainWindow in
+ // WindowAcceptsEvent isn't enough. It also has to check isClickThrough.
+ BOOL isClickThrough = (theEvent == mClickThroughMouseDownEvent);
+ [mClickThroughMouseDownEvent release];
+ mClickThroughMouseDownEvent = nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if ([self maybeRollup:theEvent] ||
+ !ChildViewMouseTracker::WindowAcceptsEvent([self window], theEvent, self, isClickThrough)) {
+ // Remember blocking because that means we want to block mouseup as well.
+ mBlockedLastMouseDown = YES;
+ return;
+ }
+
+ // in order to send gecko events we'll need a gecko widget
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ NSInteger clickCount = [theEvent clickCount];
+ if (mBlockedLastMouseDown && clickCount > 1) {
+ // Don't send a double click if the first click of the double click was
+ // blocked.
+ clickCount--;
+ }
+ geckoEvent.mClickCount = clickCount;
+
+ if (!StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ geckoEvent.IsControl()) {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ } else {
+ geckoEvent.mButton = MouseButton::ePrimary;
+ // Don't send a click if ctrl key is pressed.
+ geckoEvent.mClickEventPrevented = geckoEvent.IsControl();
+ }
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ mBlockedLastMouseDown = NO;
+
+ // XXX maybe call markedTextSelectionChanged:client: here?
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ gLastDragView = nil;
+
+ if (!mGeckoChild || mBlockedLastMouseDown) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ if (!StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ ([theEvent modifierFlags] & NSEventModifierFlagControl)) {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ } else {
+ geckoEvent.mButton = MouseButton::ePrimary;
+ }
+
+ // Remember the event's position before calling DispatchInputEvent, because
+ // that call can mutate it and convert it into a different coordinate space.
+ LayoutDeviceIntPoint pos = geckoEvent.mRefPoint;
+
+ // This might destroy our widget (and null out mGeckoChild).
+ bool defaultPrevented = (mGeckoChild->DispatchInputEvent(&geckoEvent).mContentStatus ==
+ nsEventStatus_eConsumeNoDefault);
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ // Check to see if we are double-clicking in draggable parts of the window.
+ if (!defaultPrevented && [theEvent clickCount] == 2 &&
+ !mGeckoChild->GetNonDraggableRegion().Contains(pos.x, pos.y)) {
+ if (nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick()) {
+ [[self window] performZoom:nil];
+ } else if (nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick()) {
+ [[self window] performMiniaturize:nil];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent
+ enter:(BOOL)aEnter
+ exitFrom:(WidgetMouseEvent::ExitFrom)aExitFrom {
+ if (!mGeckoChild) return;
+
+ NSPoint windowEventLocation = nsCocoaUtils::EventLocationForWindow(aEvent, [self window]);
+ NSPoint localEventLocation = [self convertPoint:windowEventLocation fromView:nil];
+
+ EventMessage msg = aEnter ? eMouseEnterIntoWidget : eMouseExitFromWidget;
+ WidgetMouseEvent event(true, msg, mGeckoChild, WidgetMouseEvent::eReal);
+ event.mRefPoint = mGeckoChild->CocoaPointsToDevPixels(localEventLocation);
+ if (event.mMessage == eMouseExitFromWidget) {
+ event.mExitFrom = Some(aExitFrom);
+ }
+ nsEventStatus status; // ignored
+ mGeckoChild->DispatchEvent(&event, status);
+}
+
+- (void)handleMouseMoved:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ // Note, sending the above event might have destroyed our widget since we didn't retain.
+ // Fine so long as we don't access any local variables from here on.
+
+ // XXX maybe call markedTextSelectionChanged:client: here?
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ [self maybeRollup:theEvent];
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ // The right mouse went down, fire off a right mouse down event to gecko
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus = mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return;
+
+ if (!StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ // Let the superclass do the context menu stuff.
+ [super rightMouseDown:theEvent];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ nsIWidget::ContentAndAPZEventStatus eventStatus = mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return;
+
+ if (StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ // Let the superclass do the context menu stuff, but pretend it's rightMouseDown.
+ NSEvent* dupeEvent = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
+ location:theEvent.locationInWindow
+ modifierFlags:theEvent.modifierFlags
+ timestamp:theEvent.timestamp
+ windowNumber:theEvent.windowNumber
+ context:nil
+ eventNumber:theEvent.eventNumber
+ clickCount:theEvent.clickCount
+ pressure:theEvent.pressure];
+
+ [super rightMouseDown:dupeEvent];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseDragged:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+
+ // send event into Gecko by going directly to the
+ // the widget.
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+static bool ShouldDispatchBackForwardCommandForMouseButton(int16_t aButton) {
+ return (aButton == MouseButton::eX1 && Preferences::GetBool("mousebutton.4th.enabled", true)) ||
+ (aButton == MouseButton::eX2 && Preferences::GetBool("mousebutton.5th.enabled", true));
+}
+
+- (void)otherMouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if ([self maybeRollup:theEvent] ||
+ !ChildViewMouseTracker::WindowAcceptsEvent([self window], theEvent, self))
+ return;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ if (ShouldDispatchBackForwardCommandForMouseButton(button)) {
+ WidgetCommandEvent appCommandEvent(
+ true, (button == MouseButton::eX2) ? nsGkAtoms::Forward : nsGkAtoms::Back, mGeckoChild);
+ mGeckoChild->DispatchWindowEvent(appCommandEvent);
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = button;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)otherMouseUp:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ if (ShouldDispatchBackForwardCommandForMouseButton(button)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = button;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+- (void)otherMouseDragged:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ geckoEvent.mButton = button;
+
+ // send event into Gecko by going directly to the
+ // the widget.
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+- (void)sendWheelStartOrStop:(EventMessage)msg forEvent:(NSEvent*)theEvent {
+ WidgetWheelEvent wheelEvent(true, msg, mGeckoChild);
+ [self convertCocoaMouseWheelEvent:theEvent toGeckoEvent:&wheelEvent];
+ mExpectingWheelStop = (msg == eWheelOperationStart);
+ mGeckoChild->DispatchInputEvent(wheelEvent.AsInputEvent());
+}
+
+- (void)sendWheelCondition:(BOOL)condition
+ first:(EventMessage)first
+ second:(EventMessage)second
+ forEvent:(NSEvent*)theEvent {
+ if (mExpectingWheelStop == condition) {
+ [self sendWheelStartOrStop:first forEvent:theEvent];
+ }
+ [self sendWheelStartOrStop:second forEvent:theEvent];
+}
+
+static int32_t RoundUp(double aDouble) {
+ return aDouble < 0 ? static_cast<int32_t>(floor(aDouble)) : static_cast<int32_t>(ceil(aDouble));
+}
+
+static gfx::IntPoint GetIntegerDeltaForEvent(NSEvent* aEvent) {
+ if ([aEvent hasPreciseScrollingDeltas]) {
+ // Pixel scroll events (events with hasPreciseScrollingDeltas == YES)
+ // carry pixel deltas in the scrollingDeltaX/Y fields and line scroll
+ // information in the deltaX/Y fields.
+ // Prior to 10.12, these line scroll fields would be zero for most pixel
+ // scroll events and non-zero for some, whenever at least a full line
+ // worth of pixel scrolling had accumulated. That's the behavior we want.
+ // Starting with 10.12 however, pixel scroll events no longer accumulate
+ // deltaX and deltaY; they just report floating point values for every
+ // single event. So we need to do our own accumulation.
+ return PanGestureInput::GetIntegerDeltaForEvent([aEvent phase] == NSEventPhaseBegan,
+ [aEvent deltaX], [aEvent deltaY]);
+ }
+
+ // For line scrolls, or pre-10.12, just use the rounded up value of deltaX / deltaY.
+ return gfx::IntPoint(RoundUp([aEvent deltaX]), RoundUp([aEvent deltaY]));
+}
+
+- (void)scrollWheel:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ ChildViewMouseTracker::MouseScrolled(theEvent);
+
+ if ([self maybeRollup:theEvent]) {
+ return;
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ NSEventPhase phase = [theEvent phase];
+ // Fire eWheelOperationStart/End events when 2 fingers touch/release the
+ // touchpad.
+ if (phase & NSEventPhaseMayBegin) {
+ [self sendWheelCondition:YES
+ first:eWheelOperationEnd
+ second:eWheelOperationStart
+ forEvent:theEvent];
+ } else if (phase & (NSEventPhaseEnded | NSEventPhaseCancelled)) {
+ [self sendWheelCondition:NO
+ first:eWheelOperationStart
+ second:eWheelOperationEnd
+ forEvent:theEvent];
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+ RefPtr<nsChildView> geckoChildDeathGrip(mGeckoChild);
+
+ NSPoint locationInWindow = nsCocoaUtils::EventLocationForWindow(theEvent, [self window]);
+
+ // Use convertWindowCoordinatesRoundDown when converting the position to
+ // integer screen pixels in order to ensure that coordinates which are just
+ // inside the right / bottom edges of the window don't end up outside of the
+ // window after rounding.
+ ScreenPoint position =
+ ViewAs<ScreenPixel>([self convertWindowCoordinatesRoundDown:locationInWindow],
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ bool usePreciseDeltas = [theEvent hasPreciseScrollingDeltas] &&
+ Preferences::GetBool("mousewheel.enable_pixel_scrolling", true);
+ bool hasPhaseInformation = nsCocoaUtils::EventHasPhaseInformation(theEvent);
+
+ gfx::IntPoint lineOrPageDelta = -GetIntegerDeltaForEvent(theEvent);
+
+ Modifiers modifiers = nsCocoaUtils::ModifiersForEvent(theEvent);
+
+ TimeStamp eventTimeStamp = nsCocoaUtils::GetEventTimeStamp([theEvent timestamp]);
+
+ ScreenPoint preciseDelta;
+ if (usePreciseDeltas) {
+ CGFloat pixelDeltaX = [theEvent scrollingDeltaX];
+ CGFloat pixelDeltaY = [theEvent scrollingDeltaY];
+ double scale = geckoChildDeathGrip->BackingScaleFactor();
+ preciseDelta = ScreenPoint(-pixelDeltaX * scale, -pixelDeltaY * scale);
+ }
+
+ if (usePreciseDeltas && hasPhaseInformation) {
+ PanGestureInput panEvent = nsCocoaUtils::CreatePanGestureEvent(
+ theEvent, eventTimeStamp, position, preciseDelta, lineOrPageDelta, modifiers);
+
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(panEvent);
+ } else if (usePreciseDeltas) {
+ // This is on 10.6 or old touchpads that don't have any phase information.
+ ScrollWheelInput wheelEvent(eventTimeStamp, modifiers, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, position, preciseDelta.x,
+ preciseDelta.y, false,
+ // This parameter is used for wheel delta
+ // adjustment, such as auto-dir scrolling,
+ // but we do't need to do anything special here
+ // since this wheel event is sent to
+ // DispatchAPZWheelInputEvent, which turns this
+ // ScrollWheelInput back into a WidgetWheelEvent
+ // and then it goes through the regular handling
+ // in APZInputBridge. So passing |eNone| won't
+ // pass up the necessary wheel delta adjustment.
+ WheelDeltaAdjustmentStrategy::eNone);
+ wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x;
+ wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y;
+ wheelEvent.mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(theEvent);
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent);
+ } else {
+ ScrollWheelInput::ScrollMode scrollMode = ScrollWheelInput::SCROLLMODE_INSTANT;
+ if (StaticPrefs::general_smoothScroll() && StaticPrefs::general_smoothScroll_mouseWheel()) {
+ scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
+ }
+ ScrollWheelInput wheelEvent(eventTimeStamp, modifiers, scrollMode,
+ ScrollWheelInput::SCROLLDELTA_LINE, position, lineOrPageDelta.x,
+ lineOrPageDelta.y, false,
+ // This parameter is used for wheel delta
+ // adjustment, such as auto-dir scrolling,
+ // but we do't need to do anything special here
+ // since this wheel event is sent to
+ // DispatchAPZWheelInputEvent, which turns this
+ // ScrollWheelInput back into a WidgetWheelEvent
+ // and then it goes through the regular handling
+ // in APZInputBridge. So passing |eNone| won't
+ // pass up the necessary wheel delta adjustment.
+ WheelDeltaAdjustmentStrategy::eNone);
+ wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x;
+ wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y;
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSMenu*)menuForEvent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mGeckoChild) return nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ [self maybeRollup:theEvent];
+ if (!mGeckoChild) return nil;
+
+ // Cocoa doesn't always dispatch a mouseDown: for a control-click event,
+ // depends on what we return from menuForEvent:. Gecko always expects one
+ // and expects the mouse down event before the context menu event, so
+ // get that event sent first if this is a left mouse click.
+ if ([theEvent type] == NSEventTypeLeftMouseDown) {
+ [self mouseDown:theEvent];
+ if (!mGeckoChild) return nil;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eContextMenu, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ if (StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ [theEvent type] == NSEventTypeLeftMouseDown) {
+ geckoEvent.mContextMenuTrigger = WidgetMouseEvent::eControlClick;
+ geckoEvent.mButton = MouseButton::ePrimary;
+ } else {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ }
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return nil;
+
+ [self maybeInitContextMenuTracking];
+
+ // We never return an actual NSMenu* for the context menu. Gecko might have
+ // responded to the eContextMenu event by putting up a fake context menu.
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)willOpenMenu:(NSMenu*)aMenu withEvent:(NSEvent*)aEvent {
+ ChildViewMouseTracker::NativeMenuOpened();
+}
+
+- (void)didCloseMenu:(NSMenu*)aMenu withEvent:(NSEvent*)aEvent {
+ ChildViewMouseTracker::NativeMenuClosed();
+}
+
+- (void)convertCocoaMouseWheelEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetWheelEvent*)outWheelEvent {
+ [self convertCocoaMouseEvent:aMouseEvent toGeckoEvent:outWheelEvent];
+
+ bool usePreciseDeltas = [aMouseEvent hasPreciseScrollingDeltas] &&
+ Preferences::GetBool("mousewheel.enable_pixel_scrolling", true);
+
+ outWheelEvent->mDeltaMode = usePreciseDeltas ? dom::WheelEvent_Binding::DOM_DELTA_PIXEL
+ : dom::WheelEvent_Binding::DOM_DELTA_LINE;
+ outWheelEvent->mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(aMouseEvent);
+}
+
+- (void)convertCocoaMouseEvent:(NSEvent*)aMouseEvent toGeckoEvent:(WidgetInputEvent*)outGeckoEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ASSERTION(outGeckoEvent,
+ "convertCocoaMouseEvent:toGeckoEvent: requires non-null aoutGeckoEvent");
+ if (!outGeckoEvent) return;
+
+ nsCocoaUtils::InitInputEvent(*outGeckoEvent, aMouseEvent);
+
+ // convert point to view coordinate system
+ NSPoint locationInWindow = nsCocoaUtils::EventLocationForWindow(aMouseEvent, [self window]);
+
+ outGeckoEvent->mRefPoint = [self convertWindowCoordinates:locationInWindow];
+
+ WidgetMouseEventBase* mouseEvent = outGeckoEvent->AsMouseEventBase();
+ mouseEvent->mButtons = 0;
+ NSUInteger mouseButtons = [NSEvent pressedMouseButtons];
+
+ if (mouseButtons & 0x01) {
+ mouseEvent->mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ }
+ if (mouseButtons & 0x02) {
+ mouseEvent->mButtons |= MouseButtonsFlag::eSecondaryFlag;
+ }
+ if (mouseButtons & 0x04) {
+ mouseEvent->mButtons |= MouseButtonsFlag::eMiddleFlag;
+ }
+ if (mouseButtons & 0x08) {
+ mouseEvent->mButtons |= MouseButtonsFlag::e4thFlag;
+ }
+ if (mouseButtons & 0x10) {
+ mouseEvent->mButtons |= MouseButtonsFlag::e5thFlag;
+ }
+
+ switch ([aMouseEvent type]) {
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeOtherMouseDragged:
+ case NSEventTypeMouseMoved:
+ if ([aMouseEvent subtype] == NSEventSubtypeTabletPoint) {
+ [self convertCocoaTabletPointerEvent:aMouseEvent toGeckoEvent:mouseEvent->AsMouseEvent()];
+ }
+ break;
+
+ default:
+ // Don't check other NSEvents for pressure.
+ break;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)convertCocoaTabletPointerEvent:(NSEvent*)aPointerEvent
+ toGeckoEvent:(WidgetMouseEvent*)aOutGeckoEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+ if (!aOutGeckoEvent || !sIsTabletPointerActivated) {
+ return;
+ }
+ if ([aPointerEvent type] != NSEventTypeMouseMoved) {
+ aOutGeckoEvent->mPressure = [aPointerEvent pressure];
+ MOZ_ASSERT(aOutGeckoEvent->mPressure >= 0.0 && aOutGeckoEvent->mPressure <= 1.0);
+ }
+ aOutGeckoEvent->mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_PEN;
+ aOutGeckoEvent->tiltX = (int32_t)lround([aPointerEvent tilt].x * 90);
+ aOutGeckoEvent->tiltY = (int32_t)lround([aPointerEvent tilt].y * 90);
+ aOutGeckoEvent->tangentialPressure = [aPointerEvent tangentialPressure];
+ // Make sure the twist value is in the range of 0-359.
+ int32_t twist = (int32_t)fmod([aPointerEvent rotation], 360);
+ aOutGeckoEvent->twist = twist >= 0 ? twist : twist + 360;
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)tabletProximity:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+ sIsTabletPointerActivated = [theEvent isEnteringProximity];
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+#pragma mark -
+// NSTextInputClient implementation
+
+- (NSRange)markedRange {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0));
+ return mTextInputHandler->MarkedRange();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRange(0, 0));
+}
+
+- (NSRange)selectedRange {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0));
+ return mTextInputHandler->SelectedRange();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRange(0, 0));
+}
+
+- (BOOL)drawsVerticallyForCharacterAtIndex:(NSUInteger)charIndex {
+ NS_ENSURE_TRUE(mTextInputHandler, NO);
+ if (charIndex == NSNotFound) {
+ return NO;
+ }
+ return mTextInputHandler->DrawsVerticallyForCharacterAtIndex(charIndex);
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
+ NS_ENSURE_TRUE(mTextInputHandler, 0);
+ return mTextInputHandler->CharacterIndexForPoint(thePoint);
+}
+
+- (NSArray*)validAttributesForMarkedText {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, [NSArray array]);
+ return mTextInputHandler->GetValidAttributesForMarkedText();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE_VOID(mGeckoChild);
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ NSAttributedString* attrStr;
+ if ([aString isKindOfClass:[NSAttributedString class]]) {
+ attrStr = static_cast<NSAttributedString*>(aString);
+ } else {
+ attrStr = [[[NSAttributedString alloc] initWithString:aString] autorelease];
+ }
+
+ mTextInputHandler->InsertText(attrStr, &replacementRange);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild || !mTextInputHandler) {
+ return;
+ }
+
+ const char* sel = reinterpret_cast<const char*>(aSelector);
+ if (!mTextInputHandler->DoCommandBySelector(sel)) {
+ [super doCommandBySelector:aSelector];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)unmarkText {
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+ mTextInputHandler->CommitIMEComposition();
+}
+
+- (BOOL)hasMarkedText {
+ NS_ENSURE_TRUE(mTextInputHandler, NO);
+ return mTextInputHandler->HasMarkedText();
+}
+
+- (void)setMarkedText:(id)aString
+ selectedRange:(NSRange)selectedRange
+ replacementRange:(NSRange)replacementRange {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ NSAttributedString* attrStr;
+ if ([aString isKindOfClass:[NSAttributedString class]]) {
+ attrStr = static_cast<NSAttributedString*>(aString);
+ } else {
+ attrStr = [[[NSAttributedString alloc] initWithString:aString] autorelease];
+ }
+
+ mTextInputHandler->SetMarkedText(attrStr, selectedRange, &replacementRange);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)aRange
+ actualRange:(NSRangePointer)actualRange {
+ NS_ENSURE_TRUE(mTextInputHandler, nil);
+ return mTextInputHandler->GetAttributedSubstringFromRange(aRange, actualRange);
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange {
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRect(0.0, 0.0, 0.0, 0.0));
+ return mTextInputHandler->FirstRectForCharacterRange(aRange, actualRange);
+}
+
+- (void)quickLookWithEvent:(NSEvent*)event {
+ // Show dictionary by current point
+ WidgetContentCommandEvent contentCommandEvent(true, eContentCommandLookUpDictionary, mGeckoChild);
+ NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
+ contentCommandEvent.mRefPoint = mGeckoChild->CocoaPointsToDevPixels(point);
+ mGeckoChild->DispatchWindowEvent(contentCommandEvent);
+ // The widget might have been destroyed.
+}
+
+- (NSInteger)windowLevel {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, [[self window] level]);
+ return mTextInputHandler->GetWindowLevel();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNormalWindowLevel);
+}
+
+#pragma mark -
+
+// This is a private API that Cocoa uses.
+// Cocoa will call this after the menu system returns "NO" for "performKeyEquivalent:".
+// We want all they key events we can get so just return YES. In particular, this fixes
+// ctrl-tab - we don't get a "keyDown:" call for that without this.
+- (BOOL)_wantsKeyDownForEvent:(NSEvent*)event {
+ return YES;
+}
+
+- (NSEvent*)lastKeyDownEvent {
+ return mLastKeyDownEvent;
+}
+
+- (void)keyDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mLastKeyDownEvent release];
+ mLastKeyDownEvent = [theEvent retain];
+
+ // Weird things can happen on keyboard input if the key window isn't in the
+ // current space. For example see bug 1056251. To get around this, always
+ // make sure that, if our window is key, it's also made frontmost. Doing
+ // this automatically switches to whatever space our window is in. Safari
+ // does something similar. Our window should normally always be key --
+ // otherwise why is the OS sending us a key down event? But it's just
+ // possible we're in Gecko's hidden window, so we check first.
+ NSWindow* viewWindow = [self window];
+ if (viewWindow && [viewWindow isKeyWindow]) {
+ [viewWindow orderWindow:NSWindowAbove relativeTo:0];
+ }
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ if (!Preferences::GetBool("intl.allow-insecure-text-input", false) && mGeckoChild &&
+ mTextInputHandler && mTextInputHandler->IsFocused()) {
+ NSWindow* window = [self window];
+ NSString* info = [NSString
+ stringWithFormat:
+ @"\nview [%@], window [%@], window is key %i, is fullscreen %i, app is active %i", self,
+ window, [window isKeyWindow], ([window styleMask] & NSWindowStyleMaskFullScreen) != 0,
+ [NSApp isActive]];
+ nsAutoCString additionalInfo([info UTF8String]);
+
+ if (mGeckoChild->GetInputContext().IsPasswordEditor() &&
+ !TextInputHandler::IsSecureEventInputEnabled()) {
+# define CRASH_MESSAGE "A password editor has focus, but not in secure input mode"
+
+ CrashReporter::AppendAppNotesToCrashReport("\nBug 893973: "_ns +
+ nsLiteralCString(CRASH_MESSAGE));
+ CrashReporter::AppendAppNotesToCrashReport(additionalInfo);
+
+ MOZ_CRASH(CRASH_MESSAGE);
+# undef CRASH_MESSAGE
+ } else if (!mGeckoChild->GetInputContext().IsPasswordEditor() &&
+ TextInputHandler::IsSecureEventInputEnabled()) {
+# define CRASH_MESSAGE "A non-password editor has focus, but in secure input mode"
+
+ CrashReporter::AppendAppNotesToCrashReport("\nBug 893973: "_ns +
+ nsLiteralCString(CRASH_MESSAGE));
+ CrashReporter::AppendAppNotesToCrashReport(additionalInfo);
+
+ MOZ_CRASH(CRASH_MESSAGE);
+# undef CRASH_MESSAGE
+ }
+ }
+#endif // #if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ if (mGeckoChild) {
+ if (mTextInputHandler) {
+ sUniqueKeyEventId++;
+ NSMutableDictionary* nativeKeyEventsMap = [ChildView sNativeKeyEventsMap];
+ [nativeKeyEventsMap setObject:theEvent forKey:@(sUniqueKeyEventId)];
+ // Purge old native events, in case we're still holding on to them. We
+ // keep at most 10 references to 10 different native events.
+ [nativeKeyEventsMap removeObjectForKey:@(sUniqueKeyEventId - 10)];
+ mTextInputHandler->HandleKeyDownEvent(theEvent, sUniqueKeyEventId);
+ } else {
+ // There was no text input handler. Offer the event to the native menu
+ // system to check if there are any registered custom shortcuts for this
+ // event.
+ mGeckoChild->SendEventToNativeMenuSystem(theEvent);
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)keyUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(mGeckoChild, );
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ mTextInputHandler->HandleKeyUpEvent(theEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)insertNewline:(id)sender {
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertParagraph);
+ }
+}
+
+- (void)insertLineBreak:(id)sender {
+ // Ctrl + Enter in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertLineBreak);
+ }
+}
+
+- (void)deleteBackward:(id)sender {
+ // Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharBackward);
+ }
+}
+
+- (void)deleteBackwardByDecomposingPreviousCharacter:(id)sender {
+ // Ctrl + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharBackward);
+ }
+}
+
+- (void)deleteWordBackward:(id)sender {
+ // Alt + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteWordBackward);
+ }
+}
+
+- (void)deleteToBeginningOfBackward:(id)sender {
+ // Command + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteToBeginningOfLine);
+ }
+}
+
+- (void)deleteForward:(id)sender {
+ // Delete in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharForward);
+ }
+}
+
+- (void)deleteWordForward:(id)sender {
+ // Alt + Delete in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteWordForward);
+ }
+}
+
+- (void)insertTab:(id)sender {
+ // Tab in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertTab);
+ }
+}
+
+- (void)insertBacktab:(id)sender {
+ // Shift + Tab in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertBacktab);
+ }
+}
+
+- (void)moveRight:(id)sender {
+ // RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::CharNext);
+ }
+}
+
+- (void)moveRightAndModifySelection:(id)sender {
+ // Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectCharNext);
+ }
+}
+
+- (void)moveWordRight:(id)sender {
+ // Alt + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::WordNext);
+ }
+}
+
+- (void)moveWordRightAndModifySelection:(id)sender {
+ // Alt + Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectWordNext);
+ }
+}
+
+- (void)moveToRightEndOfLine:(id)sender {
+ // Command + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::EndLine);
+ }
+}
+
+- (void)moveToRightEndOfLineAndModifySelection:(id)sender {
+ // Command + Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectEndLine);
+ }
+}
+
+- (void)moveLeft:(id)sender {
+ // LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::CharPrevious);
+ }
+}
+
+- (void)moveLeftAndModifySelection:(id)sender {
+ // Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectCharPrevious);
+ }
+}
+
+- (void)moveWordLeft:(id)sender {
+ // Alt + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::WordPrevious);
+ }
+}
+
+- (void)moveWordLeftAndModifySelection:(id)sender {
+ // Alt + Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectWordPrevious);
+ }
+}
+
+- (void)moveToLeftEndOfLine:(id)sender {
+ // Command + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::BeginLine);
+ }
+}
+
+- (void)moveToLeftEndOfLineAndModifySelection:(id)sender {
+ // Command + Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectBeginLine);
+ }
+}
+
+- (void)moveUp:(id)sender {
+ // ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::LinePrevious);
+ }
+}
+
+- (void)moveUpAndModifySelection:(id)sender {
+ // Shift + ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectLinePrevious);
+ }
+}
+
+- (void)moveToBeginningOfDocument:(id)sender {
+ // Command + ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::MoveTop);
+ }
+}
+
+- (void)moveToBeginningOfDocumentAndModifySelection:(id)sender {
+ // Command + Shift + ArrowUp or Shift + Home in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectTop);
+ }
+}
+
+- (void)moveDown:(id)sender {
+ // ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::LineNext);
+ }
+}
+
+- (void)moveDownAndModifySelection:(id)sender {
+ // Shift + ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectLineNext);
+ }
+}
+
+- (void)moveToEndOfDocument:(id)sender {
+ // Command + ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::MoveBottom);
+ }
+}
+
+- (void)moveToEndOfDocumentAndModifySelection:(id)sender {
+ // Command + Shift + ArrowDown or Shift + End in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectBottom);
+ }
+}
+
+- (void)scrollPageUp:(id)sender {
+ // PageUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollPageUp);
+ }
+}
+
+- (void)pageUpAndModifySelection:(id)sender {
+ // Shift + PageUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectPageUp);
+ }
+}
+
+- (void)scrollPageDown:(id)sender {
+ // PageDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollPageDown);
+ }
+}
+
+- (void)pageDownAndModifySelection:(id)sender {
+ // Shift + PageDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectPageDown);
+ }
+}
+
+- (void)scrollToEndOfDocument:(id)sender {
+ // End in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollBottom);
+ }
+}
+
+- (void)scrollToBeginningOfDocument:(id)sender {
+ // Home in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollTop);
+ }
+}
+
+// XXX Don't decleare nor implement calcelOperation: because it
+// causes not calling keyDown: for Command + Period.
+// We need to handle it from doCommandBySelector:.
+
+- (void)complete:(id)sender {
+ // Alt + Escape or Alt + Shift + Escape in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::Complete);
+ }
+}
+
+- (void)flagsChanged:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(mGeckoChild, );
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mTextInputHandler->HandleFlagsChanged(theEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isFirstResponder {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSResponder* resp = [[self window] firstResponder];
+ return (resp == (NSResponder*)self);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (BOOL)isDragInProgress {
+ if (!mDragService) return NO;
+
+ nsCOMPtr<nsIDragSession> dragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(dragSession));
+ return dragSession != nullptr;
+}
+
+- (BOOL)inactiveWindowAcceptsMouseEvent:(NSEvent*)aEvent {
+ // If we're being destroyed assume the default -- return YES.
+ if (!mGeckoChild) return YES;
+
+ WidgetMouseEvent geckoEvent(true, eMouseActivate, mGeckoChild, WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:aEvent toGeckoEvent:&geckoEvent];
+ return (mGeckoChild->DispatchInputEvent(&geckoEvent).mContentStatus !=
+ nsEventStatus_eConsumeNoDefault);
+}
+
+// We must always call through to our superclass, even when mGeckoChild is
+// nil -- otherwise the keyboard focus can end up in the wrong NSView.
+- (BOOL)becomeFirstResponder {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [super becomeFirstResponder];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(YES);
+}
+
+- (void)viewsWindowDidBecomeKey {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ // check to see if the window implements the mozWindow protocol. This
+ // allows embedders to avoid re-entrant calls to -makeKeyAndOrderFront,
+ // which can happen because these activate calls propagate out
+ // to the embedder via nsIEmbeddingSiteWindow::SetFocus().
+ BOOL isMozWindow = [[self window] respondsToSelector:@selector(setSuppressMakeKeyFront:)];
+ if (isMozWindow) [[self window] setSuppressMakeKeyFront:YES];
+
+ nsIWidgetListener* listener = mGeckoChild->GetWidgetListener();
+ if (listener) listener->WindowActivated();
+
+ if (isMozWindow) [[self window] setSuppressMakeKeyFront:NO];
+
+ if (mGeckoChild->GetInputContext().IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)viewsWindowDidResignKey {
+ if (!mGeckoChild) return;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ nsIWidgetListener* listener = mGeckoChild->GetWidgetListener();
+ if (listener) listener->WindowDeactivated();
+
+ TextInputHandler::EnsureSecureEventInputDisabled();
+}
+
+// If the call to removeFromSuperview isn't delayed from nsChildView::
+// TearDownView(), the NSView hierarchy might get changed during calls to
+// [ChildView drawRect:], which leads to "beyond bounds" exceptions in
+// NSCFArray. For more info see bmo bug 373122. Apple's docs claim that
+// removeFromSuperviewWithoutNeedingDisplay "can be safely invoked during
+// display" (whatever "display" means). But it's _not_ true that it can be
+// safely invoked during calls to [NSView drawRect:]. We use
+// removeFromSuperview here because there's no longer any danger of being
+// "invoked during display", and because doing do clears up bmo bug 384343.
+- (void)delayedTearDown {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self removeFromSuperview];
+ [self release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// drag'n'drop stuff
+#define kDragServiceContractID "@mozilla.org/widget/dragservice;1"
+
+- (NSDragOperation)dragOperationFromDragAction:(int32_t)aDragAction {
+ if (nsIDragService::DRAGDROP_ACTION_LINK & aDragAction) return NSDragOperationLink;
+ if (nsIDragService::DRAGDROP_ACTION_COPY & aDragAction) return NSDragOperationCopy;
+ if (nsIDragService::DRAGDROP_ACTION_MOVE & aDragAction) return NSDragOperationGeneric;
+ return NSDragOperationNone;
+}
+
+- (LayoutDeviceIntPoint)convertWindowCoordinates:(NSPoint)aPoint {
+ if (!mGeckoChild) {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+
+ NSPoint localPoint = [self convertPoint:aPoint fromView:nil];
+ return mGeckoChild->CocoaPointsToDevPixels(localPoint);
+}
+
+- (LayoutDeviceIntPoint)convertWindowCoordinatesRoundDown:(NSPoint)aPoint {
+ if (!mGeckoChild) {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+
+ NSPoint localPoint = [self convertPoint:aPoint fromView:nil];
+ return mGeckoChild->CocoaPointsToDevPixelsRoundDown(localPoint);
+}
+
+// This is a utility function used by NSView drag event methods
+// to send events. It contains all of the logic needed for Gecko
+// dragging to work. Returns the appropriate cocoa drag operation code.
+- (NSDragOperation)doDragAction:(EventMessage)aMessage sender:(id)aSender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mGeckoChild) return NSDragOperationNone;
+
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView doDragAction: entered\n"));
+
+ if (!mDragService) {
+ CallGetService(kDragServiceContractID, &mDragService);
+ NS_ASSERTION(mDragService, "Couldn't get a drag service - big problem!");
+ if (!mDragService) return NSDragOperationNone;
+ }
+
+ if (aMessage == eDragEnter) {
+ mDragService->StartDragSession();
+ }
+
+ nsCOMPtr<nsIDragSession> dragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (dragSession) {
+ if (aMessage == eDragOver) {
+ // fire the drag event at the source. Just ignore whether it was
+ // cancelled or not as there isn't actually a means to stop the drag
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->FireDragEventAtSource(eDrag,
+ nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ dragSession->SetCanDrop(false);
+ } else if (aMessage == eDrop) {
+ // We make the assumption that the dragOver handlers have correctly set
+ // the |canDrop| property of the Drag Session.
+ bool canDrop = false;
+ if (!NS_SUCCEEDED(dragSession->GetCanDrop(&canDrop)) || !canDrop) {
+ [self doDragAction:eDragExit sender:aSender];
+
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(false, nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ }
+ return NSDragOperationNone;
+ }
+ }
+
+ unsigned int modifierFlags = [[NSApp currentEvent] modifierFlags];
+ uint32_t action = nsIDragService::DRAGDROP_ACTION_MOVE;
+ // force copy = option, alias = cmd-option, default is move
+ if (modifierFlags & NSEventModifierFlagOption) {
+ if (modifierFlags & NSEventModifierFlagCommand)
+ action = nsIDragService::DRAGDROP_ACTION_LINK;
+ else
+ action = nsIDragService::DRAGDROP_ACTION_COPY;
+ }
+ dragSession->SetDragAction(action);
+ }
+
+ // set up gecko event
+ WidgetDragEvent geckoEvent(true, aMessage, mGeckoChild);
+ nsCocoaUtils::InitInputEvent(geckoEvent, [NSApp currentEvent]);
+
+ // Use our own coordinates in the gecko event.
+ // Convert event from gecko global coords to gecko view coords.
+ NSPoint draggingLoc = [aSender draggingLocation];
+
+ geckoEvent.mRefPoint = [self convertWindowCoordinates:draggingLoc];
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return NSDragOperationNone;
+
+ if (dragSession) {
+ switch (aMessage) {
+ case eDragEnter:
+ case eDragOver: {
+ uint32_t dragAction;
+ dragSession->GetDragAction(&dragAction);
+
+ // If TakeChildProcessDragAction returns something other than
+ // DRAGDROP_ACTION_UNINITIALIZED, it means that the last event was sent
+ // to the child process and this event is also being sent to the child
+ // process. In this case, use the last event's action instead.
+ nsDragService* dragService = static_cast<nsDragService*>(mDragService);
+ int32_t childDragAction = dragService->TakeChildProcessDragAction();
+ if (childDragAction != nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) {
+ dragAction = childDragAction;
+ }
+
+ return [self dragOperationFromDragAction:dragAction];
+ }
+ case eDragExit:
+ case eDrop: {
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ // We're leaving a window while doing a drag that was
+ // initiated in a different app. End the drag session,
+ // since we're done with it for now (until the user
+ // drags back into mozilla).
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(false, nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ return NSDragOperationGeneric;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSDragOperationNone);
+}
+
+// NSDraggingDestination
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingEntered: entered\n"));
+
+ // there should never be a globalDragPboard when "draggingEntered:" is
+ // called, but just in case we'll take care of it here.
+ [globalDragPboard release];
+
+ // Set the global drag pasteboard that will be used for this drag session.
+ // This will be set back to nil when the drag session ends (mouse exits
+ // the view or a drop happens within the view).
+ globalDragPboard = [[sender draggingPasteboard] retain];
+
+ return [self doDragAction:eDragEnter sender:sender];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSDragOperationNone);
+}
+
+// NSDraggingDestination
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingUpdated: entered\n"));
+ return [self doDragAction:eDragOver sender:sender];
+}
+
+// NSDraggingDestination
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingExited: entered\n"));
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ [self doDragAction:eDragExit sender:sender];
+ NS_IF_RELEASE(mDragService);
+}
+
+// NSDraggingDestination
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ BOOL handled = [self doDragAction:eDrop sender:sender] != NSDragOperationNone;
+ NS_IF_RELEASE(mDragService);
+ return handled;
+}
+
+// NSDraggingSource
+// This is just implemented so we comply with the NSDraggingSource protocol.
+- (NSDragOperation)draggingSession:(NSDraggingSession*)session
+ sourceOperationMaskForDraggingContext:(NSDraggingContext)context {
+ return UINT_MAX;
+}
+
+// NSDraggingSource
+- (BOOL)ignoreModifierKeysForDraggingSession:(NSDraggingSession*)session {
+ return YES;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession
+ endedAtPoint:(NSPoint)aPoint
+ operation:(NSDragOperation)aOperation {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ gDraggedTransferables = nullptr;
+
+ NSEvent* currentEvent = [NSApp currentEvent];
+ gUserCancelledDrag =
+ ([currentEvent type] == NSEventTypeKeyDown && [currentEvent keyCode] == kVK_Escape);
+
+ if (!mDragService) {
+ CallGetService(kDragServiceContractID, &mDragService);
+ NS_ASSERTION(mDragService, "Couldn't get a drag service - big problem!");
+ }
+
+ if (mDragService) {
+ RefPtr<nsDragService> dragService = static_cast<nsDragService*>(mDragService);
+
+ // Set the dragend point from the current mouse location
+ // FIXME(emilio): Weird that we wouldn't use aPoint instead? Seems to work
+ // locally as well...
+ // NSPoint pnt = aPoint;
+ NSPoint pnt = [NSEvent mouseLocation];
+ NSPoint locationInWindow = nsCocoaUtils::ConvertPointFromScreen([self window], pnt);
+ FlipCocoaScreenCoordinate(pnt);
+ dragService->SetDragEndPoint([self convertWindowCoordinates:locationInWindow]);
+
+ // XXX: dropEffect should be updated per |aOperation|.
+ // As things stand though, |aOperation| isn't well handled within "our"
+ // events, that is, when the drop happens within the window: it is set
+ // either to NSDragOperationGeneric or to NSDragOperationNone.
+ // For that reason, it's not yet possible to override dropEffect per the
+ // given OS value, and it's also unclear what's the correct dropEffect
+ // value for NSDragOperationGeneric that is passed by other applications.
+ // All that said, NSDragOperationNone is still reliable.
+ if (aOperation == NSDragOperationNone) {
+ RefPtr<dom::DataTransfer> dataTransfer = dragService->GetDataTransfer();
+ if (dataTransfer) {
+ dataTransfer->SetDropEffectInt(nsIDragService::DRAGDROP_ACTION_NONE);
+ }
+ }
+
+ dragService->EndDragSession(true, nsCocoaUtils::ModifiersForEvent(currentEvent));
+ NS_RELEASE(mDragService);
+ }
+
+ [globalDragPboard release];
+ globalDragPboard = nil;
+ [gLastDragMouseDownEvent release];
+ gLastDragMouseDownEvent = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession movedToPoint:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Get the drag service if it isn't already cached. The drag service
+ // isn't cached when dragging over a different application.
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ if (!dragService) {
+ dragService = do_GetService(kDragServiceContractID);
+ }
+
+ if (dragService) {
+ nsDragService* ds = static_cast<nsDragService*>(dragService.get());
+ ds->DragMovedWithView(aSession, aPoint);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession willBeginAtPoint:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // there should never be a globalDragPboard when "willBeginAtPoint:" is
+ // called, but just in case we'll take care of it here.
+ [globalDragPboard release];
+
+ // Set the global drag pasteboard that will be used for this drag session.
+ // This will be set back to nil when the drag session ends (mouse exits
+ // the view or a drop happens within the view).
+ globalDragPboard = [[aSession draggingPasteboard] retain];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Get the paste location from the low level pasteboard.
+static CFTypeRefPtr<CFURLRef> GetPasteLocation(NSPasteboard* aPasteboard) {
+ PasteboardRef pboardRef = nullptr;
+ PasteboardCreate((CFStringRef)[aPasteboard name], &pboardRef);
+ if (!pboardRef) {
+ return nullptr;
+ }
+
+ auto pasteBoard = CFTypeRefPtr<PasteboardRef>::WrapUnderCreateRule(pboardRef);
+ PasteboardSynchronize(pasteBoard.get());
+
+ CFURLRef urlRef = nullptr;
+ PasteboardCopyPasteLocation(pasteBoard.get(), &urlRef);
+ return CFTypeRefPtr<CFURLRef>::WrapUnderCreateRule(urlRef);
+}
+
+// NSPasteboardItemDataProvider
+- (void)pasteboard:(NSPasteboard*)aPasteboard
+ item:(NSPasteboardItem*)aItem
+ provideDataForType:(NSString*)aType {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ if (!gDraggedTransferables) {
+ return;
+ }
+
+ uint32_t count = 0;
+ gDraggedTransferables->GetLength(&count);
+
+ for (uint32_t j = 0; j < count; j++) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(gDraggedTransferables, j);
+ if (!currentTransferable) {
+ return;
+ }
+
+ // Transform the transferable to an NSDictionary.
+ NSDictionary* pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(currentTransferable);
+ if (!pasteboardOutputDict) {
+ return;
+ }
+
+ // Write everything out to the pasteboard.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* types = [NSMutableArray arrayWithCapacity:typeCount + 1];
+ [types addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ [types addObject:[UTIHelper stringFromPboardType:kMozWildcardPboardType]];
+ for (unsigned int k = 0; k < typeCount; k++) {
+ NSString* curType = [types objectAtIndex:k];
+ if ([curType isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeString]] ||
+ [curType isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlPboardType]] ||
+ [curType isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlNamePboardType]] ||
+ [curType isEqualToString:[UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]]) {
+ [aPasteboard setString:[pasteboardOutputDict valueForKey:curType] forType:curType];
+ } else if ([curType
+ isEqualToString:[UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType]]) {
+ [aPasteboard setPropertyList:[pasteboardOutputDict valueForKey:curType] forType:curType];
+ } else if ([curType isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeHTML]]) {
+ [aPasteboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(
+ [pasteboardOutputDict valueForKey:curType]))
+ forType:curType];
+ } else if ([curType isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeTIFF]] ||
+ [curType
+ isEqualToString:[UTIHelper stringFromPboardType:kMozCustomTypesPboardType]]) {
+ [aPasteboard setData:[pasteboardOutputDict valueForKey:curType] forType:curType];
+ } else if ([curType
+ isEqualToString:[UTIHelper stringFromPboardType:kMozFileUrlsPboardType]]) {
+ [aPasteboard writeObjects:[pasteboardOutputDict valueForKey:curType]];
+ } else if ([curType
+ isEqualToString:[UTIHelper
+ stringFromPboardType:(NSString*)
+ kPasteboardTypeFileURLPromise]]) {
+ nsCOMPtr<nsIFile> targFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(targFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(targFile);
+ if (!macLocalFile) {
+ NS_ERROR("No Mac local file");
+ continue;
+ }
+
+ CFTypeRefPtr<CFURLRef> url = GetPasteLocation(aPasteboard);
+ if (!url) {
+ continue;
+ }
+
+ if (!NS_SUCCEEDED(macLocalFile->InitWithCFURL(url.get()))) {
+ NS_ERROR("failed InitWithCFURL");
+ continue;
+ }
+
+ if (!gDraggedTransferables) {
+ continue;
+ }
+
+ uint32_t transferableCount;
+ nsresult rv = gDraggedTransferables->GetLength(&transferableCount);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ for (uint32_t i = 0; i < transferableCount; i++) {
+ nsCOMPtr<nsITransferable> item = do_QueryElementAt(gDraggedTransferables, i);
+ if (!item) {
+ NS_ERROR("no transferable");
+ continue;
+ }
+
+ item->SetTransferData(kFilePromiseDirectoryMime, macLocalFile);
+
+ // Now request the kFilePromiseMime data, which will invoke the data
+ // provider. If successful, the file will have been created.
+ nsCOMPtr<nsISupports> fileDataPrimitive;
+ Unused << item->GetTransferData(kFilePromiseMime, getter_AddRefs(fileDataPrimitive));
+ }
+
+ [aPasteboard setPropertyList:[pasteboardOutputDict valueForKey:curType] forType:curType];
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// Support for the "Services" menu. We currently only support sending strings
+// and HTML to system services.
+// This method can be called on any thread (see bug 1751687). We can only usefully
+// handle it on the main thread.
+- (id)validRequestorForSendType:(NSString*)sendType returnType:(NSString*)returnType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!NS_IsMainThread()) {
+ // We don't have any thread-safe ways of checking whether we can send
+ // or receive content. Just say no. In normal cases, we expect this
+ // method to be called on the main thread.
+ return [super validRequestorForSendType:sendType returnType:returnType];
+ }
+
+ // sendType contains the type of data that the service would like this
+ // application to send to it. sendType is nil if the service is not
+ // requesting any data.
+ //
+ // returnType contains the type of data the the service would like to
+ // return to this application (e.g., to overwrite the selection).
+ // returnType is nil if the service will not return any data.
+ //
+ // The following condition thus triggers when the service expects a string
+ // or HTML from us or no data at all AND when the service will either not
+ // send back any data to us or will send a string or HTML back to us.
+
+ id result = nil;
+
+ NSString* stringType = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ NSString* htmlType = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ if ((!sendType || [sendType isEqualToString:stringType] || [sendType isEqualToString:htmlType]) &&
+ (!returnType || [returnType isEqualToString:stringType] ||
+ [returnType isEqualToString:htmlType])) {
+ if (mGeckoChild) {
+ // Assume that this object will be able to handle this request.
+ result = self;
+
+ // Keep the ChildView alive during this operation.
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if (sendType) {
+ // Determine if there is a current selection (chrome/content).
+ if (!nsClipboard::sSelectionCache) {
+ result = nil;
+ }
+ }
+
+ // Determine if we can paste (if receiving data from the service).
+ if (mGeckoChild && returnType) {
+ WidgetContentCommandEvent command(true, eContentCommandPasteTransferable, mGeckoChild,
+ true);
+ // This might possibly destroy our widget (and null out mGeckoChild).
+ mGeckoChild->DispatchWindowEvent(command);
+ if (!mGeckoChild || !command.mSucceeded || !command.mIsEnabled) result = nil;
+ }
+ }
+ }
+
+ // Give the superclass a chance if this object will not handle this request.
+ if (!result) result = [super validRequestorForSendType:sendType returnType:returnType];
+
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ // Make sure that the service will accept strings or HTML.
+ if (![types containsObject:[UTIHelper stringFromPboardType:NSStringPboardType]] &&
+ ![types containsObject:[UTIHelper stringFromPboardType:NSPasteboardTypeString]] &&
+ ![types containsObject:[UTIHelper stringFromPboardType:NSPasteboardTypeHTML]]) {
+ return NO;
+ }
+
+ // Bail out if there is no Gecko object.
+ if (!mGeckoChild) return NO;
+
+ // Transform the transferable to an NSDictionary.
+ NSDictionary* pasteboardOutputDict = nullptr;
+
+ pasteboardOutputDict = nsClipboard::PasteboardDictFromTransferable(nsClipboard::sSelectionCache);
+
+ if (!pasteboardOutputDict) return NO;
+
+ // Declare the pasteboard types.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* declaredTypes = [NSMutableArray arrayWithCapacity:typeCount];
+ [declaredTypes addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ [pboard declareTypes:declaredTypes owner:nil];
+
+ // Write the data to the pasteboard.
+ for (unsigned int i = 0; i < typeCount; i++) {
+ NSString* currentKey = [declaredTypes objectAtIndex:i];
+ id currentValue = [pasteboardOutputDict valueForKey:currentKey];
+
+ if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeString]] ||
+ [currentKey isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlPboardType]] ||
+ [currentKey isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlNamePboardType]]) {
+ [pboard setString:currentValue forType:currentKey];
+ } else if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeHTML]]) {
+ [pboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
+ forType:currentKey];
+ } else if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeTIFF]]) {
+ [pboard setData:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper
+ stringFromPboardType:(NSString*)
+ kPasteboardTypeFileURLPromise]] ||
+ [currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType]]) {
+ [pboard setPropertyList:currentValue forType:currentKey];
+ }
+ }
+ return YES;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+// Called if the service wants us to replace the current selection.
+- (BOOL)readSelectionFromPasteboard:(NSPasteboard*)pboard {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> trans = do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) return NO;
+ trans->Init(nullptr);
+
+ trans->AddDataFlavor(kTextMime);
+ trans->AddDataFlavor(kHTMLMime);
+
+ rv = nsClipboard::TransferableFromPasteboard(trans, pboard);
+ if (NS_FAILED(rv)) return NO;
+
+ NS_ENSURE_TRUE(mGeckoChild, false);
+
+ WidgetContentCommandEvent command(true, eContentCommandPasteTransferable, mGeckoChild);
+ command.mTransferable = trans;
+ mGeckoChild->DispatchWindowEvent(command);
+
+ return command.mSucceeded && command.mIsEnabled;
+}
+
+- (void)pressureChangeWithEvent:(NSEvent*)event {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ NSInteger stage = [event stage];
+ if (mLastPressureStage == 1 && stage == 2) {
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ if ([userDefaults integerForKey:@"com.apple.trackpad.forceClick"] == 1) {
+ // This is no public API to get configuration for current force click.
+ // This is filed as radar 29294285.
+ [self quickLookWithEvent:event];
+ }
+ }
+ mLastPressureStage = stage;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+nsresult nsChildView::GetSelectionAsPlaintext(nsAString& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!nsClipboard::sSelectionCache) {
+ MOZ_ASSERT(aResult.IsEmpty());
+ return NS_OK;
+ }
+
+ // Get the current chrome or content selection.
+ NSDictionary* pasteboardOutputDict = nullptr;
+ pasteboardOutputDict = nsClipboard::PasteboardDictFromTransferable(nsClipboard::sSelectionCache);
+
+ if (NS_WARN_IF(!pasteboardOutputDict)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Declare the pasteboard types.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* declaredTypes = [NSMutableArray arrayWithCapacity:typeCount];
+ [declaredTypes addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ NSString* currentKey = [declaredTypes objectAtIndex:0];
+ NSString* currentValue = [pasteboardOutputDict valueForKey:currentKey];
+ const char* textSelection = [currentValue UTF8String];
+ aResult = NS_ConvertUTF8toUTF16(textSelection);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+#ifdef DEBUG
+nsresult nsChildView::SetHiDPIMode(bool aHiDPI) {
+ nsCocoaUtils::InvalidateHiDPIState();
+ Preferences::SetInt("gfx.hidpi.enabled", aHiDPI ? 1 : 0);
+ BackingScaleFactorChanged();
+ return NS_OK;
+}
+
+nsresult nsChildView::RestoreHiDPIMode() {
+ nsCocoaUtils::InvalidateHiDPIState();
+ Preferences::ClearUser("gfx.hidpi.enabled");
+ BackingScaleFactorChanged();
+ return NS_OK;
+}
+#endif
+
+#pragma mark -
+
+#ifdef ACCESSIBILITY
+
+/* Every ChildView has a corresponding mozDocAccessible object that is doing all
+ the heavy lifting. The topmost ChildView corresponds to a mozRootAccessible
+ object.
+
+ All ChildView needs to do is to route all accessibility calls (from the NSAccessibility APIs)
+ down to its object, pretending that they are the same.
+*/
+- (id<mozAccessible>)accessible {
+ if (!mGeckoChild) return nil;
+
+ id<mozAccessible> nativeAccessible = nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ RefPtr<nsChildView> geckoChild(mGeckoChild);
+ RefPtr<a11y::LocalAccessible> accessible = geckoChild->GetDocumentAccessible();
+ if (!accessible) return nil;
+
+ accessible->GetNativeInterface((void**)&nativeAccessible);
+
+# ifdef DEBUG_hakan
+ NSAssert(![nativeAccessible isExpired], @"native acc is expired!!!");
+# endif
+
+ return nativeAccessible;
+}
+
+/* Implementation of formal mozAccessible formal protocol (enabling mozViews
+ to talk to mozAccessible objects in the accessibility module). */
+
+- (BOOL)hasRepresentedView {
+ return YES;
+}
+
+- (id)representedView {
+ return self;
+}
+
+- (BOOL)isRoot {
+ return [[self accessible] isRoot];
+}
+
+# pragma mark -
+
+// general
+
+- (BOOL)isAccessibilityElement {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super isAccessibilityElement];
+
+ return [[self accessible] isAccessibilityElement];
+}
+
+- (id)accessibilityHitTest:(NSPoint)point {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityHitTest:point];
+
+ return [[self accessible] accessibilityHitTest:point];
+}
+
+- (id)accessibilityFocusedUIElement {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityFocusedUIElement];
+
+ return [[self accessible] accessibilityFocusedUIElement];
+}
+
+// actions
+
+- (NSArray*)accessibilityActionNames {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityActionNames];
+
+ return [[self accessible] accessibilityActionNames];
+}
+
+- (NSString*)accessibilityActionDescription:(NSString*)action {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityActionDescription:action];
+
+ return [[self accessible] accessibilityActionDescription:action];
+}
+
+- (void)accessibilityPerformAction:(NSString*)action {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityPerformAction:action];
+
+ return [[self accessible] accessibilityPerformAction:action];
+}
+
+// attributes
+
+- (NSArray*)accessibilityAttributeNames {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityAttributeNames];
+
+ return [[self accessible] accessibilityAttributeNames];
+}
+
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityIsAttributeSettable:attribute];
+
+ return [[self accessible] accessibilityIsAttributeSettable:attribute];
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return [super accessibilityAttributeValue:attribute];
+
+ id<mozAccessible> accessible = [self accessible];
+
+ // if we're the root (topmost) accessible, we need to return our native AXParent as we
+ // traverse outside to the hierarchy of whoever embeds us. thus, fall back on NSView's
+ // default implementation for this attribute.
+ if ([attribute isEqualToString:NSAccessibilityParentAttribute] && [accessible isRoot]) {
+ id parentAccessible = [super accessibilityAttributeValue:attribute];
+ return parentAccessible;
+ }
+
+ return [accessible accessibilityAttributeValue:attribute];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+#endif /* ACCESSIBILITY */
+
++ (uint32_t)sUniqueKeyEventId {
+ return sUniqueKeyEventId;
+}
+
++ (NSMutableDictionary*)sNativeKeyEventsMap {
+ // This dictionary is "leaked".
+ static NSMutableDictionary* sNativeKeyEventsMap = [[NSMutableDictionary alloc] init];
+ return sNativeKeyEventsMap;
+}
+
+@end
+
+@implementation PixelHostingView
+
+- (id)initWithFrame:(NSRect)aRect {
+ self = [super initWithFrame:aRect];
+
+ self.wantsLayer = YES;
+ self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
+
+ return self;
+}
+
+- (BOOL)isFlipped {
+ return YES;
+}
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil;
+}
+
+- (void)drawRect:(NSRect)aRect {
+ NS_WARNING("Unexpected call to drawRect: This view returns YES from wantsUpdateLayer, so "
+ "drawRect should not be called.");
+}
+
+- (BOOL)wantsUpdateLayer {
+ return YES;
+}
+
+- (void)updateLayer {
+ [(ChildView*)[self superview] updateRootCALayer];
+}
+
+- (BOOL)wantsBestResolutionOpenGLSurface {
+ return nsCocoaUtils::HiDPIEnabled() ? YES : NO;
+}
+
+@end
+
+#pragma mark -
+
+void ChildViewMouseTracker::OnDestroyView(ChildView* aView) {
+ if (sLastMouseEventView == aView) {
+ sLastMouseEventView = nil;
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = nil;
+ }
+}
+
+void ChildViewMouseTracker::OnDestroyWindow(NSWindow* aWindow) {
+ if (sWindowUnderMouse == aWindow) {
+ sWindowUnderMouse = nil;
+ }
+}
+
+void ChildViewMouseTracker::MouseEnteredWindow(NSEvent* aEvent) {
+ sWindowUnderMouse = [aEvent window];
+ ReEvaluateMouseEnterState(aEvent);
+}
+
+void ChildViewMouseTracker::MouseExitedWindow(NSEvent* aEvent) {
+ if (sWindowUnderMouse == [aEvent window]) {
+ sWindowUnderMouse = nil;
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = nil;
+ ReEvaluateMouseEnterState(aEvent);
+ }
+}
+
+void ChildViewMouseTracker::NativeMenuOpened() {
+ // Send a mouse exit event now.
+ // The menu consumes all mouse events while it's open, and we don't want to be stuck thinking the
+ // mouse is still hovering our window after the mouse has already moved. This could result in
+ // unintended cursor changes or tooltips.
+ sWindowUnderMouse = nil;
+ ReEvaluateMouseEnterState(nil);
+}
+
+void ChildViewMouseTracker::NativeMenuClosed() {
+ // If a window was hovered before the menu opened, re-enter that window at the last known mouse
+ // position.
+ // After -[NSView didCloseMenu:withEvent:] is called, any NSTrackingArea updates that were
+ // buffered while the menu was open will be replayed.
+ if (sLastMouseMoveEvent) {
+ sWindowUnderMouse = sLastMouseMoveEvent.window;
+ ReEvaluateMouseEnterState(sLastMouseMoveEvent);
+ }
+}
+
+void ChildViewMouseTracker::ReEvaluateMouseEnterState(NSEvent* aEvent, ChildView* aOldView) {
+ ChildView* oldView = aOldView ? aOldView : sLastMouseEventView;
+ sLastMouseEventView = ViewForEvent(aEvent);
+ if (sLastMouseEventView != oldView) {
+ // Send enter and / or exit events.
+ WidgetMouseEvent::ExitFrom exitFrom = [sLastMouseEventView window] == [oldView window]
+ ? WidgetMouseEvent::ePlatformChild
+ : WidgetMouseEvent::ePlatformTopLevel;
+ [oldView sendMouseEnterOrExitEvent:aEvent enter:NO exitFrom:exitFrom];
+ // After the cursor exits the window set it to a visible regular arrow cursor.
+ if (exitFrom == WidgetMouseEvent::ePlatformTopLevel) {
+ [[nsCursorManager sharedInstance] setNonCustomCursor:nsIWidget::Cursor{eCursor_standard}];
+ }
+ [sLastMouseEventView sendMouseEnterOrExitEvent:aEvent enter:YES exitFrom:exitFrom];
+ }
+}
+
+void ChildViewMouseTracker::ResendLastMouseMoveEvent() {
+ if (sLastMouseMoveEvent) {
+ MouseMoved(sLastMouseMoveEvent);
+ }
+}
+
+void ChildViewMouseTracker::MouseMoved(NSEvent* aEvent) {
+ MouseEnteredWindow(aEvent);
+ [sLastMouseEventView handleMouseMoved:aEvent];
+ if (sLastMouseMoveEvent != aEvent) {
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = [aEvent retain];
+ }
+}
+
+void ChildViewMouseTracker::MouseScrolled(NSEvent* aEvent) {
+ if (!nsCocoaUtils::IsMomentumScrollEvent(aEvent)) {
+ // Store the position so we can pin future momentum scroll events.
+ sLastScrollEventScreenLocation = nsCocoaUtils::ScreenLocationForEvent(aEvent);
+ }
+}
+
+ChildView* ChildViewMouseTracker::ViewForEvent(NSEvent* aEvent) {
+ NSWindow* window = sWindowUnderMouse;
+ if (!window) return nil;
+
+ NSPoint windowEventLocation = nsCocoaUtils::EventLocationForWindow(aEvent, window);
+ NSView* view = [[[window contentView] superview] hitTest:windowEventLocation];
+
+ if (![view isKindOfClass:[ChildView class]]) return nil;
+
+ ChildView* childView = (ChildView*)view;
+ // If childView is being destroyed return nil.
+ if (![childView widget]) return nil;
+ return WindowAcceptsEvent(window, aEvent, childView) ? childView : nil;
+}
+
+BOOL ChildViewMouseTracker::WindowAcceptsEvent(NSWindow* aWindow, NSEvent* aEvent, ChildView* aView,
+ BOOL aIsClickThrough) {
+ // Right mouse down events may get through to all windows, even to a top level
+ // window with an open sheet.
+ if (!aWindow || [aEvent type] == NSEventTypeRightMouseDown) return YES;
+
+ id delegate = [aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return YES;
+
+ nsIWidget* windowWidget = [(WindowDelegate*)delegate geckoWidget];
+ if (!windowWidget) return YES;
+
+ NSWindow* topLevelWindow = nil;
+
+ switch (windowWidget->GetWindowType()) {
+ case WindowType::Popup:
+ // If this is a context menu, it won't have a parent. So we'll always
+ // accept mouse move events on context menus even when none of our windows
+ // is active, which is the right thing to do.
+ // For panels, the parent window is the XUL window that owns the panel.
+ return WindowAcceptsEvent([aWindow parentWindow], aEvent, aView, aIsClickThrough);
+
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ if ([aWindow attachedSheet]) return NO;
+
+ topLevelWindow = aWindow;
+ break;
+ case WindowType::Sheet: {
+ nsIWidget* parentWidget = windowWidget->GetSheetWindowParent();
+ if (!parentWidget) return YES;
+
+ topLevelWindow = (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW);
+ break;
+ }
+
+ default:
+ return YES;
+ }
+
+ if (!topLevelWindow || ([topLevelWindow isMainWindow] && !aIsClickThrough) ||
+ [aEvent type] == NSEventTypeOtherMouseDown ||
+ (([aEvent modifierFlags] & NSEventModifierFlagCommand) != 0 &&
+ [aEvent type] != NSEventTypeMouseMoved))
+ return YES;
+
+ // If we're here then we're dealing with a left click or mouse move on an
+ // inactive window or something similar. Ask Gecko what to do.
+ return [aView inactiveWindowAcceptsMouseEvent:aEvent];
+}
+
+#pragma mark -
diff --git a/widget/cocoa/nsClipboard.h b/widget/cocoa/nsClipboard.h
new file mode 100644
index 0000000000..c838f531d0
--- /dev/null
+++ b/widget/cocoa/nsClipboard.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsClipboard_h_
+#define nsClipboard_h_
+
+#include "nsBaseClipboard.h"
+#include "nsCOMPtr.h"
+#include "nsIClipboard.h"
+#include "nsString.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPtr.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsITransferable;
+
+class nsClipboard : public nsBaseClipboard {
+ public:
+ nsClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIClipboard
+ NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ bool* _retval) override;
+ NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override;
+
+ // On macOS, cache the transferable of the current selection (chrome/content)
+ // in the parent process. This is needed for the services menu which
+ // requires synchronous access to the current selection.
+ static mozilla::StaticRefPtr<nsITransferable> sSelectionCache;
+
+ // Helper methods, used also by nsDragService
+ static NSDictionary* PasteboardDictFromTransferable(nsITransferable* aTransferable);
+ // aPasteboardType is being retained and needs to be released by the caller.
+ static bool IsStringType(const nsCString& aMIMEType, NSString** aPasteboardType);
+ static bool IsImageType(const nsACString& aMIMEType);
+ static NSString* WrapHtmlForSystemPasteboard(NSString* aString);
+ static nsresult TransferableFromPasteboard(nsITransferable* aTransferable, NSPasteboard* pboard);
+
+ protected:
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ void ClearSelectionCache();
+ void SetSelectionCache(nsITransferable* aTransferable);
+
+ private:
+ virtual ~nsClipboard();
+
+ static mozilla::Maybe<uint32_t> FindIndexOfImageFlavor(const nsTArray<nsCString>& aMIMETypes);
+
+ int32_t mCachedClipboard = -1;
+ // Set to the native change count after any modification of the clipboard.
+ int32_t mChangeCount = 0;
+};
+
+#endif // nsClipboard_h_
diff --git a/widget/cocoa/nsClipboard.mm b/widget/cocoa/nsClipboard.mm
new file mode 100644
index 0000000000..0d1c7c688b
--- /dev/null
+++ b/widget/cocoa/nsClipboard.mm
@@ -0,0 +1,776 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include <algorithm>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+
+#include "gfxPlatform.h"
+#include "nsArrayUtils.h"
+#include "nsCOMPtr.h"
+#include "nsClipboard.h"
+#include "nsString.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsIFile.h"
+#include "nsStringStream.h"
+#include "nsEscape.h"
+#include "nsPrintfCString.h"
+#include "nsObjCExceptions.h"
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+
+using mozilla::LogLevel;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::SourceSurface;
+
+mozilla::StaticRefPtr<nsITransferable> nsClipboard::sSelectionCache;
+
+nsClipboard::nsClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(false /* supportsSelectionClipboard */,
+ true /* supportsFindClipboard */,
+ true /* supportsSelectionCache */)) {}
+
+nsClipboard::~nsClipboard() { ClearSelectionCache(); }
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsClipboard, nsBaseClipboard)
+
+namespace {
+
+// We separate this into its own function because after an @try, all local
+// variables within that function get marked as volatile, and our C++ type
+// system doesn't like volatile things.
+static NSData* GetDataFromPasteboard(NSPasteboard* aPasteboard, NSString* aType) {
+ NSData* data = nil;
+ @try {
+ data = [aPasteboard dataForType:aType];
+ } @catch (NSException* e) {
+ NS_WARNING(
+ nsPrintfCString("Exception raised while getting data from the pasteboard: \"%s - %s\"",
+ [[e name] UTF8String], [[e reason] UTF8String])
+ .get());
+ mozilla::Unused << e;
+ }
+ return data;
+}
+
+static NSPasteboard* GetPasteboard(int32_t aWhichClipboard) {
+ switch (aWhichClipboard) {
+ case nsIClipboard::kGlobalClipboard:
+ return [NSPasteboard generalPasteboard];
+ case nsIClipboard::kFindClipboard:
+ if (@available(macOS 10.13, *)) {
+ return [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
+ }
+ return [NSPasteboard pasteboardWithName:NSFindPboard];
+ default:
+ return nil;
+ }
+}
+
+} // namespace
+
+void nsClipboard::SetSelectionCache(nsITransferable* aTransferable) {
+ sSelectionCache = aTransferable;
+}
+
+void nsClipboard::ClearSelectionCache() { sSelectionCache = nullptr; }
+
+NS_IMETHODIMP
+nsClipboard::SetNativeClipboardData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner,
+ int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (aWhichClipboard == kSelectionCache) {
+ SetSelectionCache(aTransferable);
+ return NS_OK;
+ }
+
+ NSDictionary* pasteboardOutputDict = PasteboardDictFromTransferable(aTransferable);
+ if (!pasteboardOutputDict) return NS_ERROR_FAILURE;
+
+ unsigned int outputCount = [pasteboardOutputDict count];
+ NSArray* outputKeys = [pasteboardOutputDict allKeys];
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ MOZ_ASSERT(cocoaPasteboard);
+ if (aWhichClipboard == kFindClipboard) {
+ NSString* stringType = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ [cocoaPasteboard declareTypes:[NSArray arrayWithObject:stringType] owner:nil];
+ } else {
+ // Write everything else out to the general pasteboard.
+ MOZ_ASSERT(aWhichClipboard == kGlobalClipboard);
+ [cocoaPasteboard declareTypes:outputKeys owner:nil];
+ }
+
+ for (unsigned int i = 0; i < outputCount; i++) {
+ NSString* currentKey = [outputKeys objectAtIndex:i];
+ id currentValue = [pasteboardOutputDict valueForKey:currentKey];
+ if (aWhichClipboard == kFindClipboard) {
+ if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeString]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ }
+ } else {
+ if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeString]] ||
+ [currentKey isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlPboardType]] ||
+ [currentKey isEqualToString:[UTIHelper stringFromPboardType:kPublicUrlNamePboardType]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType]]) {
+ [cocoaPasteboard setPropertyList:[pasteboardOutputDict valueForKey:currentKey]
+ forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeHTML]]) {
+ [cocoaPasteboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
+ forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:kMozFileUrlsPboardType]]) {
+ [cocoaPasteboard writeObjects:currentValue];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ } else {
+ [cocoaPasteboard setData:currentValue forType:currentKey];
+ }
+ }
+ }
+
+ mCachedClipboard = aWhichClipboard;
+ mChangeCount = [cocoaPasteboard changeCount];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsClipboard::TransferableFromPasteboard(nsITransferable* aTransferable,
+ NSPasteboard* cocoaPasteboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // get flavor list that includes all acceptable flavors (including ones obtained through
+ // conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ // printf("looking for clipboard data of type %s\n", flavorStr.get());
+
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
+ NSString* pString = [cocoaPasteboard stringForType:pboardType];
+ if (!pString) {
+ continue;
+ }
+
+ NSData* stringData;
+ bool isRTF =
+ [pboardType isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeRTF]];
+ if (isRTF) {
+ stringData = [pString dataUsingEncoding:NSASCIIStringEncoding];
+ } else {
+ stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding];
+ }
+ unsigned int dataLength = [stringData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ [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(flavorStr, clipboardDataPtrNoBOM, dataLength,
+ getter_AddRefs(genericDataWrapper));
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ break;
+ } else if (flavorStr.EqualsLiteral(kFileMime)) {
+ NSArray* items = [cocoaPasteboard pasteboardItems];
+ if (!items || [items count] <= 0) {
+ continue;
+ }
+
+ // XXX we don't support multiple clipboard item on DOM and XPCOM interface
+ // for now, so we only get the data from the first pasteboard item.
+ NSPasteboardItem* item = [items objectAtIndex:0];
+ if (!item) {
+ continue;
+ }
+
+ nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable, flavorStr, item);
+ } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ NSString* type = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray arrayWithObject:[UTIHelper stringFromPboardType:kMozCustomTypesPboardType]]];
+ if (!type) {
+ continue;
+ }
+
+ NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
+ if (!pasteboardData) {
+ continue;
+ }
+
+ unsigned int dataLength = [pasteboardData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ [pasteboardData getBytes:clipboardDataPtr length:dataLength];
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(flavorStr, clipboardDataPtr, dataLength,
+ getter_AddRefs(genericDataWrapper));
+
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ } else if (flavorStr.EqualsLiteral(kJPEGImageMime) || flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime)) {
+ // Figure out if there's data on the pasteboard we can grab (sanity check)
+ NSString* type = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray arrayWithObjects:[UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
+ [UTIHelper stringFromPboardType:NSPasteboardTypePNG], nil]];
+ if (!type) continue;
+
+ // Read data off the clipboard
+ NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
+ if (!pasteboardData) continue;
+
+ // Figure out what type we're converting to
+ CFStringRef outputType = NULL;
+ if (flavorStr.EqualsLiteral(kJPEGImageMime) || flavorStr.EqualsLiteral(kJPGImageMime))
+ outputType = CFSTR("public.jpeg");
+ else if (flavorStr.EqualsLiteral(kPNGImageMime))
+ outputType = CFSTR("public.png");
+ else if (flavorStr.EqualsLiteral(kGIFImageMime))
+ outputType = CFSTR("com.compuserve.gif");
+ else
+ continue;
+
+ // Use ImageIO to interpret the data on the clipboard and transcode.
+ // Note that ImageIO, like all CF APIs, allows NULLs to propagate freely
+ // and safely in most cases (like ObjC). A notable exception is CFRelease.
+ NSDictionary* options = [NSDictionary
+ dictionaryWithObjectsAndKeys:(NSNumber*)kCFBooleanTrue, kCGImageSourceShouldAllowFloat,
+ type, kCGImageSourceTypeIdentifierHint, nil];
+ CGImageSourceRef source = nullptr;
+ if (type == [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]) {
+ NSString* urlStr = [cocoaPasteboard stringForType:type];
+ NSURL* url = [NSURL URLWithString:urlStr];
+ source = CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
+ } else {
+ source = CGImageSourceCreateWithData((CFDataRef)pasteboardData, (CFDictionaryRef)options);
+ }
+
+ NSMutableData* encodedData = [NSMutableData data];
+ CGImageDestinationRef dest =
+ CGImageDestinationCreateWithData((CFMutableDataRef)encodedData, outputType, 1, NULL);
+ CGImageDestinationAddImageFromSource(dest, source, 0, NULL);
+ bool successfullyConverted = CGImageDestinationFinalize(dest);
+
+ if (successfullyConverted) {
+ // Put the converted data in a form Gecko can understand
+ nsCOMPtr<nsIInputStream> byteStream;
+ NS_NewByteInputStream(getter_AddRefs(byteStream),
+ mozilla::Span((const char*)[encodedData bytes], [encodedData length]),
+ NS_ASSIGNMENT_COPY);
+
+ aTransferable->SetTransferData(flavorStr.get(), byteStream);
+ }
+
+ if (dest) CFRelease(dest);
+ if (source) CFRelease(source);
+
+ if (successfullyConverted)
+ break;
+ else
+ continue;
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable, int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard) || !aTransferable)
+ return NS_ERROR_FAILURE;
+
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ if (!cocoaPasteboard) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get flavor list that includes all acceptable flavors (including ones obtained through
+ // conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ // If we were the last ones to put something on the pasteboard, then just use the cached
+ // transferable. Otherwise clear it because it isn't relevant any more.
+ if (mCachedClipboard == aWhichClipboard) {
+ const auto& clipboardCache = mCaches[aWhichClipboard];
+ MOZ_ASSERT(clipboardCache);
+ if (mChangeCount == [cocoaPasteboard changeCount]) {
+ if (nsITransferable* cachedTrans = clipboardCache->GetTransferable()) {
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ nsCOMPtr<nsISupports> dataSupports;
+ rv = cachedTrans->GetTransferData(flavorStr.get(), getter_AddRefs(dataSupports));
+ if (NS_SUCCEEDED(rv)) {
+ aTransferable->SetTransferData(flavorStr.get(), dataSupports);
+ return NS_OK; // maybe try to fill in more types? Is there a point?
+ }
+ }
+ }
+ } else {
+ // Remove transferable cache only. Don't clear system clipboard.
+ clipboardCache->Clear();
+ }
+ }
+
+ // at this point we can't satisfy the request from cache data so let's look
+ // for things other people put on the system clipboard
+
+ return nsClipboard::TransferableFromPasteboard(aTransferable, cocoaPasteboard);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// returns true if we have *any* of the passed in flavors available for pasting
+NS_IMETHODIMP
+nsClipboard::HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ bool* outResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CLIPBOARD_LOG("%s: clipboard=%i", __FUNCTION__, aWhichClipboard);
+ CLIPBOARD_LOG(" Asking for content:\n");
+ for (auto& flavor : aFlavorList) {
+ CLIPBOARD_LOG(" MIME %s\n", flavor.get());
+ }
+
+ *outResult = false;
+
+ // We only support the set operation on kSelectionCache type, see bug 1835059.
+ if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard)) {
+ return NS_OK;
+ }
+
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ MOZ_ASSERT(cocoaPasteboard);
+ if (mCachedClipboard == aWhichClipboard) {
+ const auto& clipboardCache = mCaches[mCachedClipboard];
+ MOZ_ASSERT(clipboardCache);
+ if (mChangeCount != [cocoaPasteboard changeCount]) {
+ // Clear the cached transferable as it is no longer valid.
+ clipboardCache->Clear();
+ } else if (nsITransferable* cachedTrans = clipboardCache->GetTransferable()) {
+ // See if we have data for this in our cached transferable.
+ nsTArray<nsCString> flavors;
+ nsresult rv = cachedTrans->FlavorsTransferableCanImport(flavors);
+ if (NS_SUCCEEDED(rv)) {
+ if (CLIPBOARD_LOG_ENABLED()) {
+ CLIPBOARD_LOG(" Cached transferable types (nums %zu)\n", flavors.Length());
+ for (uint32_t j = 0; j < flavors.Length(); j++) {
+ CLIPBOARD_LOG(" MIME %s\n", flavors[j].get());
+ }
+ }
+
+ for (uint32_t j = 0; j < flavors.Length(); j++) {
+ const nsCString& transferableFlavorStr = flavors[j];
+
+ for (uint32_t k = 0; k < aFlavorList.Length(); k++) {
+ if (transferableFlavorStr.Equals(aFlavorList[k])) {
+ CLIPBOARD_LOG(" has %s\n", aFlavorList[k].get());
+ *outResult = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (CLIPBOARD_LOG_ENABLED()) {
+ NSArray* types = [cocoaPasteboard types];
+ uint32_t count = [types count];
+ CLIPBOARD_LOG(" Pasteboard types (nums %d)\n", count);
+ for (uint32_t i = 0; i < count; i++) {
+ NSPasteboardType type = [types objectAtIndex:i];
+ if (!type) {
+ CLIPBOARD_LOG(" failed to get MIME\n");
+ continue;
+ }
+ CLIPBOARD_LOG(" MIME %s\n", [type UTF8String]);
+ }
+ }
+
+ for (auto& mimeType : aFlavorList) {
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(mimeType, &pboardType)) {
+ NSString* availableType =
+ [cocoaPasteboard availableTypeFromArray:[NSArray arrayWithObject:pboardType]];
+ if (availableType && [availableType isEqualToString:pboardType]) {
+ CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ *outResult = true;
+ break;
+ }
+ } else if (mimeType.EqualsLiteral(kCustomTypesMime)) {
+ NSString* availableType = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray arrayWithObject:[UTIHelper stringFromPboardType:kMozCustomTypesPboardType]]];
+ if (availableType) {
+ CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ *outResult = true;
+ break;
+ }
+ } else if (mimeType.EqualsLiteral(kJPEGImageMime) || mimeType.EqualsLiteral(kJPGImageMime) ||
+ mimeType.EqualsLiteral(kPNGImageMime) || mimeType.EqualsLiteral(kGIFImageMime)) {
+ NSString* availableType = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray arrayWithObjects:[UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
+ [UTIHelper stringFromPboardType:NSPasteboardTypePNG], nil]];
+ if (availableType) {
+ CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ *outResult = true;
+ break;
+ }
+ } else if (mimeType.EqualsLiteral(kFileMime)) {
+ NSArray* items = [cocoaPasteboard pasteboardItems];
+ if (items && [items count] > 0) {
+ // XXX we only check the first pasteboard item as we only get data from
+ // first item in TransferableFromPasteboard for now.
+ if (NSPasteboardItem* item = [items objectAtIndex:0]) {
+ if (NSString *availableType = [item
+ availableTypeFromArray:
+ [NSArray arrayWithObjects:[UTIHelper
+ stringFromPboardType:(NSString*)kUTTypeFileURL],
+ nil]]) {
+ CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ *outResult = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (CLIPBOARD_LOG_ENABLED() && !(*outResult)) {
+ CLIPBOARD_LOG(" no targets at clipboard (bad match)\n");
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// static
+mozilla::Maybe<uint32_t> nsClipboard::FindIndexOfImageFlavor(
+ const nsTArray<nsCString>& aMIMETypes) {
+ for (uint32_t i = 0; i < aMIMETypes.Length(); ++i) {
+ if (nsClipboard::IsImageType(aMIMETypes[i])) {
+ return mozilla::Some(i);
+ }
+ }
+
+ return mozilla::Nothing();
+}
+
+// This function converts anything that other applications might understand into the system format
+// and puts it into a dictionary which it returns.
+// static
+NSDictionary* nsClipboard::PasteboardDictFromTransferable(nsITransferable* aTransferable) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!aTransferable) {
+ return nil;
+ }
+
+ NSMutableDictionary* pasteboardOutputDict = [NSMutableDictionary dictionary];
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ const mozilla::Maybe<uint32_t> imageFlavorIndex = nsClipboard::FindIndexOfImageFlavor(flavors);
+
+ if (imageFlavorIndex) {
+ // When right-clicking and "Copy Image" is clicked on macOS, some apps expect the
+ // first flavor to be the image flavor. See bug 1689992. For other apps, the
+ // order shouldn't matter.
+ std::swap(*flavors.begin(), flavors[*imageFlavorIndex]);
+ }
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ CLIPBOARD_LOG("writing out clipboard data of type %s (%d)\n", flavorStr.get(), i);
+
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ rv = aTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(genericDataWrapper));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsAutoString data;
+ if (nsCOMPtr<nsISupportsString> text = do_QueryInterface(genericDataWrapper)) {
+ text->GetData(data);
+ }
+
+ NSString* nativeString;
+ if (!data.IsEmpty()) {
+ nativeString = [NSString stringWithCharacters:(const unichar*)data.get()
+ length:data.Length()];
+ } else {
+ nativeString = [NSString string];
+ }
+
+ // be nice to Carbon apps, normalize the receiver's contents using Form C.
+ nativeString = [nativeString precomposedStringWithCanonicalMapping];
+ if (nativeString) {
+ [pasteboardOutputDict setObject:nativeString forKey:pboardType];
+ }
+ } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ rv = aTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(genericDataWrapper));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsAutoCString data;
+ if (nsCOMPtr<nsISupportsCString> text = do_QueryInterface(genericDataWrapper)) {
+ text->GetData(data);
+ }
+
+ if (!data.IsEmpty()) {
+ NSData* nativeData = [NSData dataWithBytes:data.get() length:data.Length()];
+ NSString* customType = [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
+ if (!nativeData) {
+ continue;
+ }
+ [pasteboardOutputDict setObject:nativeData forKey:customType];
+ }
+ } else if (nsClipboard::IsImageType(flavorStr)) {
+ nsCOMPtr<nsISupports> transferSupports;
+ rv = aTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(transferSupports));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<imgIContainer> image(do_QueryInterface(transferSupports));
+ if (!image) {
+ NS_WARNING("Image isn't an imgIContainer in transferable");
+ continue;
+ }
+
+ RefPtr<SourceSurface> surface =
+ image->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ if (!surface) {
+ continue;
+ }
+ CGImageRef imageRef = NULL;
+ rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef);
+ if (NS_FAILED(rv) || !imageRef) {
+ continue;
+ }
+
+ // Convert the CGImageRef to TIFF data.
+ CFMutableDataRef tiffData = CFDataCreateMutable(kCFAllocatorDefault, 0);
+ CGImageDestinationRef destRef =
+ CGImageDestinationCreateWithData(tiffData, CFSTR("public.tiff"), 1, NULL);
+ CGImageDestinationAddImage(destRef, imageRef, NULL);
+ bool successfullyConverted = CGImageDestinationFinalize(destRef);
+
+ CGImageRelease(imageRef);
+ if (destRef) {
+ CFRelease(destRef);
+ }
+
+ if (!successfullyConverted || !tiffData) {
+ if (tiffData) {
+ CFRelease(tiffData);
+ }
+ continue;
+ }
+
+ NSString* tiffType = [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF];
+ [pasteboardOutputDict setObject:(NSMutableData*)tiffData forKey:tiffType];
+ CFRelease(tiffData);
+ } else if (flavorStr.EqualsLiteral(kFileMime)) {
+ nsCOMPtr<nsISupports> genericFile;
+ rv = aTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(genericFile));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> file(do_QueryInterface(genericFile));
+ if (!file) {
+ continue;
+ }
+
+ nsAutoString fileURI;
+ rv = file->GetPath(fileURI);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ NSString* str = nsCocoaUtils::ToNSString(fileURI);
+ NSURL* url = [NSURL fileURLWithPath:str isDirectory:NO];
+ if (!url || ![url absoluteString]) {
+ continue;
+ }
+ NSString* fileUTType = [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
+ [pasteboardOutputDict setObject:[url absoluteString] forKey:fileUTType];
+ } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) {
+ NSString* urlPromise =
+ [UTIHelper stringFromPboardType:(NSString*)kPasteboardTypeFileURLPromise];
+ NSString* urlPromiseContent =
+ [UTIHelper stringFromPboardType:(NSString*)kPasteboardTypeFilePromiseContent];
+ [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""] forKey:urlPromise];
+ [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""] forKey:urlPromiseContent];
+ } else if (flavorStr.EqualsLiteral(kURLMime)) {
+ nsCOMPtr<nsISupports> genericURL;
+ rv = aTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(genericURL));
+ nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
+
+ nsAutoString url;
+ urlObject->GetData(url);
+
+ NSString* nativeTitle = nil;
+
+ // A newline embedded in the URL means that the form is actually URL +
+ // title. This embedding occurs in nsDragService::GetData.
+ int32_t newlinePos = url.FindChar(char16_t('\n'));
+ if (newlinePos >= 0) {
+ url.Truncate(newlinePos);
+
+ nsAutoString urlTitle;
+ urlObject->GetData(urlTitle);
+ urlTitle.Mid(urlTitle, newlinePos + 1, urlTitle.Length() - (newlinePos + 1));
+
+ nativeTitle =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(urlTitle.get())
+ length:urlTitle.Length()];
+ }
+ // The Finder doesn't like getting random binary data aka
+ // Unicode, so change it into an escaped URL containing only
+ // ASCII.
+ nsAutoCString utf8Data = NS_ConvertUTF16toUTF8(url.get(), url.Length());
+ nsAutoCString escData;
+ NS_EscapeURL(utf8Data.get(), utf8Data.Length(), esc_OnlyNonASCII | esc_AlwaysCopy, escData);
+
+ NSString* nativeURL = [NSString stringWithUTF8String:escData.get()];
+ NSString* publicUrl = [UTIHelper stringFromPboardType:kPublicUrlPboardType];
+ if (!nativeURL) {
+ continue;
+ }
+ [pasteboardOutputDict setObject:nativeURL forKey:publicUrl];
+ if (nativeTitle) {
+ NSArray* urlsAndTitles = @[ @[ nativeURL ], @[ nativeTitle ] ];
+ NSString* urlName = [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
+ NSString* urlsWithTitles = [UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType];
+ [pasteboardOutputDict setObject:nativeTitle forKey:urlName];
+ [pasteboardOutputDict setObject:urlsAndTitles forKey:urlsWithTitles];
+ }
+ }
+ // If it wasn't a type that we recognize as exportable we don't put it on the system
+ // clipboard. We'll just access it from our cached transferable when we need it.
+ }
+
+ return pasteboardOutputDict;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+bool nsClipboard::IsStringType(const nsCString& aMIMEType, NSString** aPboardType) {
+ if (aMIMEType.EqualsLiteral(kTextMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ return true;
+ } else if (aMIMEType.EqualsLiteral(kRTFMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
+ return true;
+ } else if (aMIMEType.EqualsLiteral(kHTMLMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ return true;
+ } else {
+ return false;
+ }
+}
+
+// static
+bool nsClipboard::IsImageType(const nsACString& aMIMEType) {
+ return aMIMEType.EqualsLiteral(kPNGImageMime) || aMIMEType.EqualsLiteral(kJPEGImageMime) ||
+ aMIMEType.EqualsLiteral(kJPGImageMime) || aMIMEType.EqualsLiteral(kGIFImageMime) ||
+ aMIMEType.EqualsLiteral(kNativeImageMime);
+}
+
+NSString* nsClipboard::WrapHtmlForSystemPasteboard(NSString* aString) {
+ NSString* wrapped = [NSString
+ stringWithFormat:@"<html>"
+ "<head>"
+ "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">"
+ "</head>"
+ "<body>"
+ "%@"
+ "</body>"
+ "</html>",
+ aString];
+ return wrapped;
+}
+
+NS_IMETHODIMP
+nsClipboard::EmptyClipboard(int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mEmptyingForSetData) {
+ if (aWhichClipboard == kSelectionCache) {
+ ClearSelectionCache();
+ } else {
+ if (NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard)) {
+ [cocoaPasteboard clearContents];
+ }
+ if (mCachedClipboard == aWhichClipboard) {
+ mCachedClipboard = -1;
+ mChangeCount = 0;
+ }
+ }
+ }
+
+ return nsBaseClipboard::EmptyClipboard(aWhichClipboard);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsCocoaFeatures.h b/widget/cocoa/nsCocoaFeatures.h
new file mode 100644
index 0000000000..e998e82658
--- /dev/null
+++ b/widget/cocoa/nsCocoaFeatures.h
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+#ifndef nsCocoaFeatures_h_
+#define nsCocoaFeatures_h_
+
+#include <stdint.h>
+
+/// Note that this class assumes we support the platform we are running on.
+/// For better or worse, if the version is unknown or less than what we
+/// support, we set it to the minimum supported version. GetSystemVersion
+/// is the only call that returns the unadjusted values.
+class nsCocoaFeatures {
+ public:
+ static int32_t macOSVersion();
+ static int32_t macOSVersionMajor();
+ static int32_t macOSVersionMinor();
+ static int32_t macOSVersionBugFix();
+ static bool OnSierraExactly();
+ static bool OnHighSierraOrLater();
+ static bool OnMojaveOrLater();
+ static bool OnCatalinaOrLater();
+ static bool OnBigSurOrLater();
+ static bool OnMontereyOrLater();
+ static bool OnVenturaOrLater();
+
+ static bool IsAtLeastVersion(int32_t aMajor, int32_t aMinor,
+ int32_t aBugFix = 0);
+
+ static bool ProcessIsRosettaTranslated();
+
+ // These are utilities that do not change or depend on the value of
+ // mOSVersion and instead just encapsulate the encoding algorithm. Note that
+ // GetVersion actually adjusts to the lowest supported OS, so it will always
+ // return a "supported" version. GetSystemVersion does not make any
+ // modifications.
+ static void GetSystemVersion(int& aMajor, int& aMinor, int& aBugFix);
+ static int32_t GetVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix);
+ static int32_t ExtractMajorVersion(int32_t aVersion);
+ static int32_t ExtractMinorVersion(int32_t aVersion);
+ static int32_t ExtractBugFixVersion(int32_t aVersion);
+
+ private:
+ nsCocoaFeatures() = delete; // Prevent instantiation.
+ static void InitializeVersionNumbers();
+
+ static int32_t mOSVersion;
+};
+
+// C-callable helper for cairo-quartz-font.c and SkFontHost_mac.cpp
+extern "C" {
+bool Gecko_OnSierraExactly();
+}
+
+#endif // nsCocoaFeatures_h_
diff --git a/widget/cocoa/nsCocoaFeatures.mm b/widget/cocoa/nsCocoaFeatures.mm
new file mode 100644
index 0000000000..cf68755078
--- /dev/null
+++ b/widget/cocoa/nsCocoaFeatures.mm
@@ -0,0 +1,220 @@
+/* -*- 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/. */
+
+// This file makes some assumptions about the versions of macOS.
+// We are assuming that the major, minor and bugfix versions are each less than
+// 256.
+// There are MOZ_ASSERTs for that.
+
+// The formula for the version integer is (major << 16) + (minor << 8) + bugfix.
+
+#define MACOS_VERSION_MASK 0x00FFFFFF
+#define MACOS_MAJOR_VERSION_MASK 0x00FFFFFF
+#define MACOS_MINOR_VERSION_MASK 0x00FFFFFF
+#define MACOS_BUGFIX_VERSION_MASK 0x00FFFFFF
+#define MACOS_VERSION_10_0_HEX 0x000A0000
+#define MACOS_VERSION_10_9_HEX 0x000A0900
+#define MACOS_VERSION_10_10_HEX 0x000A0A00
+#define MACOS_VERSION_10_11_HEX 0x000A0B00
+#define MACOS_VERSION_10_12_HEX 0x000A0C00
+#define MACOS_VERSION_10_13_HEX 0x000A0D00
+#define MACOS_VERSION_10_14_HEX 0x000A0E00
+#define MACOS_VERSION_10_15_HEX 0x000A0F00
+#define MACOS_VERSION_10_16_HEX 0x000A1000
+#define MACOS_VERSION_11_0_HEX 0x000B0000
+#define MACOS_VERSION_12_0_HEX 0x000C0000
+#define MACOS_VERSION_13_0_HEX 0x000D0000
+
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "nsDebug.h"
+#include "nsObjCExceptions.h"
+
+#import <Cocoa/Cocoa.h>
+#include <sys/sysctl.h>
+
+/*static*/ int32_t nsCocoaFeatures::mOSVersion = 0;
+
+// This should not be called with unchecked aMajor, which should be >= 10.
+inline int32_t AssembleVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix) {
+ MOZ_ASSERT(aMajor >= 10);
+ return (aMajor << 16) + (aMinor << 8) + aBugFix;
+}
+
+int32_t nsCocoaFeatures::ExtractMajorVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return (aVersion & 0xFF0000) >> 16;
+}
+
+int32_t nsCocoaFeatures::ExtractMinorVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return (aVersion & 0xFF00) >> 8;
+}
+
+int32_t nsCocoaFeatures::ExtractBugFixVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return aVersion & 0xFF;
+}
+
+static int intAtStringIndex(NSArray* array, int index) {
+ return [(NSString*)[array objectAtIndex:index] integerValue];
+}
+
+void nsCocoaFeatures::GetSystemVersion(int& major, int& minor, int& bugfix) {
+ major = minor = bugfix = 0;
+
+ NSString* versionString = [[NSDictionary
+ dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"]
+ objectForKey:@"ProductVersion"];
+ if (!versionString) {
+ NS_ERROR("Couldn't read /System/Library/CoreServices/SystemVersion.plist to determine macOS "
+ "version.");
+ return;
+ }
+ NSArray* versions = [versionString componentsSeparatedByString:@"."];
+ NSUInteger count = [versions count];
+ if (count > 0) {
+ major = intAtStringIndex(versions, 0);
+ if (count > 1) {
+ minor = intAtStringIndex(versions, 1);
+ if (count > 2) {
+ bugfix = intAtStringIndex(versions, 2);
+ }
+ }
+ }
+}
+
+int32_t nsCocoaFeatures::GetVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix) {
+ int32_t macOSVersion;
+ if (aMajor < 10) {
+ aMajor = 10;
+ NS_ERROR("Couldn't determine macOS version, assuming 10.9");
+ macOSVersion = MACOS_VERSION_10_9_HEX;
+ } else if (aMajor == 10 && aMinor < 9) {
+ aMinor = 9;
+ NS_ERROR("macOS version too old, assuming 10.9");
+ macOSVersion = MACOS_VERSION_10_9_HEX;
+ } else {
+ MOZ_ASSERT(aMajor >= 10);
+ MOZ_ASSERT(aMajor < 256);
+ MOZ_ASSERT(aMinor >= 0);
+ MOZ_ASSERT(aMinor < 256);
+ MOZ_ASSERT(aBugFix >= 0);
+ MOZ_ASSERT(aBugFix < 256);
+ macOSVersion = AssembleVersion(aMajor, aMinor, aBugFix);
+ }
+ MOZ_ASSERT(aMajor == ExtractMajorVersion(macOSVersion));
+ MOZ_ASSERT(aMinor == ExtractMinorVersion(macOSVersion));
+ MOZ_ASSERT(aBugFix == ExtractBugFixVersion(macOSVersion));
+ return macOSVersion;
+}
+
+/*static*/ void nsCocoaFeatures::InitializeVersionNumbers() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Provide an autorelease pool to avoid leaking Cocoa objects,
+ // as this gets called before the main autorelease pool is in place.
+ nsAutoreleasePool localPool;
+
+ int major, minor, bugfix;
+ GetSystemVersion(major, minor, bugfix);
+ mOSVersion = GetVersion(major, minor, bugfix);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersion() {
+ // Don't let this be called while we're first setting the value...
+ MOZ_ASSERT((mOSVersion & MACOS_VERSION_MASK) >= 0);
+ if (!mOSVersion) {
+ mOSVersion = -1;
+ InitializeVersionNumbers();
+ }
+ return mOSVersion;
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionMajor() {
+ return ExtractMajorVersion(macOSVersion());
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionMinor() {
+ return ExtractMinorVersion(macOSVersion());
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionBugFix() {
+ return ExtractBugFixVersion(macOSVersion());
+}
+
+/* static */ bool nsCocoaFeatures::OnSierraExactly() {
+ return (macOSVersion() >= MACOS_VERSION_10_12_HEX) && (macOSVersion() < MACOS_VERSION_10_13_HEX);
+}
+
+/* Version of OnSierraExactly as global function callable from cairo & skia */
+bool Gecko_OnSierraExactly() { return nsCocoaFeatures::OnSierraExactly(); }
+
+/* static */ bool nsCocoaFeatures::OnHighSierraOrLater() {
+ return (macOSVersion() >= MACOS_VERSION_10_13_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::OnMojaveOrLater() {
+ return (macOSVersion() >= MACOS_VERSION_10_14_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::OnCatalinaOrLater() {
+ return (macOSVersion() >= MACOS_VERSION_10_15_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::OnBigSurOrLater() {
+ // Account for the version being 10.16 or 11.0 on Big Sur.
+ // The version is reported as 10.16 if SYSTEM_VERSION_COMPAT is set to 1,
+ // or if SYSTEM_VERSION_COMPAT is not set and the application is linked
+ // with a pre-Big Sur SDK.
+ // Firefox sets SYSTEM_VERSION_COMPAT to 0 in its Info.plist, so it'll
+ // usually see the correct 11.* version, despite being linked against an
+ // old SDK. However, it still sees the 10.16 compatibility version when
+ // launched from the command line, see bug 1727624. (This only applies to
+ // the Intel build - the arm64 build is linked against a Big Sur SDK and
+ // always sees the correct version.)
+ return ((macOSVersion() >= MACOS_VERSION_10_16_HEX) ||
+ (macOSVersion() >= MACOS_VERSION_11_0_HEX));
+}
+
+/* static */ bool nsCocoaFeatures::OnMontereyOrLater() {
+ // This check only works if SYSTEM_VERSION_COMPAT is off, otherwise
+ // Monterey pretends to be 10.16 and is indistinguishable from Big Sur.
+ // In practice, this means that an Intel Firefox build can return false
+ // from this function if it's launched from the command line, see bug 1727624.
+ // This will not be an issue anymore once we link against the Big Sur SDK.
+ return (macOSVersion() >= MACOS_VERSION_12_0_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::OnVenturaOrLater() {
+ // See comments above regarding SYSTEM_VERSION_COMPAT.
+ return (macOSVersion() >= MACOS_VERSION_13_0_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::IsAtLeastVersion(int32_t aMajor, int32_t aMinor,
+ int32_t aBugFix) {
+ return macOSVersion() >= GetVersion(aMajor, aMinor, aBugFix);
+}
+
+/*
+ * Returns true if the process is running under Rosetta translation. Returns
+ * false if running natively or if an error was encountered. We use the
+ * `sysctl.proc_translated` sysctl which is documented by Apple to be used
+ * for this purpose. Note: using this in a sandboxed process requires allowing
+ * the sysctl in the sandbox policy.
+ */
+/* static */ bool nsCocoaFeatures::ProcessIsRosettaTranslated() {
+ int ret = 0;
+ size_t size = sizeof(ret);
+ if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) {
+ if (errno != ENOENT) {
+ fprintf(stderr, "Failed to check for translation environment\n");
+ }
+ return false;
+ }
+ return (ret == 1);
+}
diff --git a/widget/cocoa/nsCocoaUtils.h b/widget/cocoa/nsCocoaUtils.h
new file mode 100644
index 0000000000..30332cf8a6
--- /dev/null
+++ b/widget/cocoa/nsCocoaUtils.h
@@ -0,0 +1,574 @@
+/* -*- 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/. */
+
+#ifndef nsCocoaUtils_h_
+#define nsCocoaUtils_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "InputData.h"
+#include "nsRect.h"
+#include "imgIContainer.h"
+#include "nsTArray.h"
+#include "Units.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIWidget.h"
+
+// Declare the backingScaleFactor method that we want to call
+// on NSView/Window/Screen objects, if they recognize it.
+@interface NSObject (BackingScaleFactorCategory)
+- (CGFloat)backingScaleFactor;
+@end
+
+// Pasteborad types
+extern NSString* const kPublicUrlPboardType;
+extern NSString* const kPublicUrlNamePboardType;
+extern NSString* const kUrlsWithTitlesPboardType;
+extern NSString* const kMozWildcardPboardType;
+extern NSString* const kMozCustomTypesPboardType;
+extern NSString* const kMozFileUrlsPboardType;
+
+@interface UTIHelper : NSObject
++ (NSString*)stringFromPboardType:(NSString*)aType;
+@end
+
+class nsITransferable;
+class nsIWidget;
+
+namespace mozilla {
+class TimeStamp;
+namespace gfx {
+class SourceSurface;
+} // namespace gfx
+namespace dom {
+class Promise;
+} // namespace dom
+} // namespace mozilla
+
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+
+// Used to retain a Cocoa object for the remainder of a method's execution.
+class nsAutoRetainCocoaObject {
+ public:
+ explicit nsAutoRetainCocoaObject(id anObject) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ mObject = [anObject retain];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+ ~nsAutoRetainCocoaObject() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [mObject release];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+ private:
+ id mObject; // [STRONG]
+};
+
+// Provide a local autorelease pool for the remainder of a method's execution.
+class nsAutoreleasePool {
+ public:
+ nsAutoreleasePool() { mLocalPool = [[NSAutoreleasePool alloc] init]; }
+ ~nsAutoreleasePool() { [mLocalPool release]; }
+
+ private:
+ NSAutoreleasePool* mLocalPool;
+};
+
+@interface NSApplication (Undocumented)
+
+// Present in all versions of OS X from (at least) 10.2.8 through 10.5.
+- (BOOL)_isRunningModal;
+- (BOOL)_isRunningAppModal;
+
+// Send an event to the current Cocoa app-modal session. Present in all
+// versions of OS X from (at least) 10.2.8 through 10.5.
+- (void)_modalSession:(NSModalSession)aSession sendEvent:(NSEvent*)theEvent;
+
+@end
+
+struct KeyBindingsCommand {
+ SEL selector;
+ id data;
+};
+
+@interface NativeKeyBindingsRecorder : NSResponder {
+ @private
+ nsTArray<KeyBindingsCommand>* mCommands;
+}
+
+- (void)startRecording:(nsTArray<KeyBindingsCommand>&)aCommands;
+
+- (void)doCommandBySelector:(SEL)aSelector;
+
+- (void)insertText:(id)aString;
+
+@end // NativeKeyBindingsRecorder
+
+#if !defined(MAC_OS_X_VERSION_10_14) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_14
+typedef NSString* AVMediaType;
+#endif
+
+class nsCocoaUtils {
+ typedef mozilla::gfx::SourceSurface SourceSurface;
+ typedef mozilla::LayoutDeviceIntPoint LayoutDeviceIntPoint;
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+ typedef mozilla::dom::Promise Promise;
+ typedef StaticAutoPtr<nsTArray<RefPtr<Promise>>> PromiseArray;
+
+ public:
+ // Get the backing scale factor from an object that supports this selector
+ // (NSView/Window/Screen, on 10.7 or later), returning 1.0 if not supported
+ static CGFloat GetBackingScaleFactor(id aObject) {
+ if (HiDPIEnabled() && [aObject respondsToSelector:@selector(backingScaleFactor)]) {
+ return [aObject backingScaleFactor];
+ }
+ return 1.0;
+ }
+
+ // Conversions between Cocoa points and device pixels, given the backing
+ // scale factor from a view/window/screen.
+ static int32_t CocoaPointsToDevPixels(CGFloat aPts, CGFloat aBackingScale) {
+ return NSToIntRound(aPts * aBackingScale);
+ }
+
+ static LayoutDeviceIntPoint CocoaPointsToDevPixels(const NSPoint& aPt, CGFloat aBackingScale) {
+ return LayoutDeviceIntPoint(NSToIntRound(aPt.x * aBackingScale),
+ NSToIntRound(aPt.y * aBackingScale));
+ }
+
+ static LayoutDeviceIntPoint CocoaPointsToDevPixelsRoundDown(const NSPoint& aPt,
+ CGFloat aBackingScale) {
+ return LayoutDeviceIntPoint(NSToIntFloor(aPt.x * aBackingScale),
+ NSToIntFloor(aPt.y * aBackingScale));
+ }
+
+ static LayoutDeviceIntRect CocoaPointsToDevPixels(const NSRect& aRect, CGFloat aBackingScale) {
+ return LayoutDeviceIntRect(NSToIntRound(aRect.origin.x * aBackingScale),
+ NSToIntRound(aRect.origin.y * aBackingScale),
+ NSToIntRound(aRect.size.width * aBackingScale),
+ NSToIntRound(aRect.size.height * aBackingScale));
+ }
+
+ static CGFloat DevPixelsToCocoaPoints(int32_t aPixels, CGFloat aBackingScale) {
+ return (CGFloat)aPixels / aBackingScale;
+ }
+
+ static NSPoint DevPixelsToCocoaPoints(const mozilla::LayoutDeviceIntPoint& aPt,
+ CGFloat aBackingScale) {
+ return NSMakePoint((CGFloat)aPt.x / aBackingScale, (CGFloat)aPt.y / aBackingScale);
+ }
+
+ // Implements an NSPoint equivalent of -[NSWindow convertRectFromScreen:].
+ static NSPoint ConvertPointFromScreen(NSWindow* aWindow, const NSPoint& aPt) {
+ return [aWindow convertRectFromScreen:NSMakeRect(aPt.x, aPt.y, 0, 0)].origin;
+ }
+
+ // Implements an NSPoint equivalent of -[NSWindow convertRectToScreen:].
+ static NSPoint ConvertPointToScreen(NSWindow* aWindow, const NSPoint& aPt) {
+ return [aWindow convertRectToScreen:NSMakeRect(aPt.x, aPt.y, 0, 0)].origin;
+ }
+
+ static NSRect DevPixelsToCocoaPoints(const LayoutDeviceIntRect& aRect, CGFloat aBackingScale) {
+ return NSMakeRect((CGFloat)aRect.X() / aBackingScale, (CGFloat)aRect.Y() / aBackingScale,
+ (CGFloat)aRect.Width() / aBackingScale,
+ (CGFloat)aRect.Height() / aBackingScale);
+ }
+
+ // Returns the given y coordinate, which must be in screen coordinates,
+ // flipped from Gecko to Cocoa or Cocoa to Gecko.
+ static float FlippedScreenY(float y);
+
+ // The following functions come in "DevPix" variants that work with
+ // backing-store (device pixel) coordinates, as well as the original
+ // versions that expect coordinates in Cocoa points/CSS pixels.
+ // The difference becomes important in HiDPI display modes, where Cocoa
+ // points and backing-store pixels are no longer 1:1.
+
+ // Gecko rects (nsRect) contain an origin (x,y) in a coordinate
+ // system with (0,0) in the top-left of the primary screen. Cocoa rects
+ // (NSRect) contain an origin (x,y) in a coordinate system with (0,0)
+ // in the bottom-left of the primary screen. Both nsRect and NSRect
+ // contain width/height info, with no difference in their use.
+ // This function does no scaling, so the Gecko coordinates are
+ // expected to be desktop pixels, which are equal to Cocoa points
+ // (by definition).
+ static NSRect GeckoRectToCocoaRect(const mozilla::DesktopIntRect& geckoRect);
+ static NSPoint GeckoPointToCocoaPoint(const mozilla::DesktopPoint& aPoint);
+
+ // Converts aGeckoRect in dev pixels to points in Cocoa coordinates
+ static NSRect GeckoRectToCocoaRectDevPix(const mozilla::LayoutDeviceIntRect& aGeckoRect,
+ CGFloat aBackingScale);
+
+ // See explanation for geckoRectToCocoaRect, guess what this does...
+ static mozilla::DesktopIntRect CocoaRectToGeckoRect(const NSRect& cocoaRect);
+
+ static mozilla::LayoutDeviceIntRect CocoaRectToGeckoRectDevPix(const NSRect& aCocoaRect,
+ CGFloat aBackingScale);
+
+ // Gives the location for the event in screen coordinates. Do not call this
+ // unless the window the event was originally targeted at is still alive!
+ // anEvent may be nil -- in that case the current mouse location is returned.
+ static NSPoint ScreenLocationForEvent(NSEvent* anEvent);
+
+ // Determines if an event happened over a window, whether or not the event
+ // is for the window. Does not take window z-order into account.
+ static BOOL IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow);
+
+ // Events are set up so that their coordinates refer to the window to which they
+ // were originally sent. If we reroute the event somewhere else, we'll have
+ // to get the window coordinates this way. Do not call this unless the window
+ // the event was originally targeted at is still alive!
+ static NSPoint EventLocationForWindow(NSEvent* anEvent, NSWindow* aWindow);
+
+ static BOOL IsMomentumScrollEvent(NSEvent* aEvent);
+ static BOOL EventHasPhaseInformation(NSEvent* aEvent);
+
+ // Hides the Menu bar and the Dock. Multiple hide/show requests can be nested.
+ static void HideOSChromeOnScreen(bool aShouldHide);
+
+ static nsIWidget* GetHiddenWindowWidget();
+
+ /**
+ * Should the application restore its state because it was launched by the OS
+ * at login?
+ */
+ static BOOL ShouldRestoreStateDueToLaunchAtLogin();
+
+ static void PrepareForNativeAppModalDialog();
+ static void CleanUpAfterNativeAppModalDialog();
+
+ // 3 utility functions to go from a frame of imgIContainer to CGImage and then to NSImage
+ // Convert imgIContainer -> CGImageRef, caller owns result
+
+ /** Creates a <code>CGImageRef</code> from a frame contained in an <code>imgIContainer</code>.
+ Copies the pixel data from the indicated frame of the <code>imgIContainer</code> into a new
+ <code>CGImageRef</code>. The caller owns the <code>CGImageRef</code>.
+ @param aFrame the frame to convert
+ @param aResult the resulting CGImageRef
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateCGImageFromSurface(SourceSurface* aSurface, CGImageRef* aResult,
+ bool* aIsEntirelyBlack = nullptr);
+
+ /** Creates a Cocoa <code>NSImage</code> from a <code>CGImageRef</code>.
+ Copies the pixel data from the <code>CGImageRef</code> into a new <code>NSImage</code>.
+ The caller owns the <code>NSImage</code>.
+ @param aInputImage the image to convert
+ @param aResult the resulting NSImage
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateNSImageFromCGImage(CGImageRef aInputImage, NSImage** aResult);
+
+ /** Creates a Cocoa <code>NSImage</code> from a frame of an <code>imgIContainer</code>.
+ Combines the two methods above. The caller owns the <code>NSImage</code>.
+ @param aImage the image to extract a frame from
+ @param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
+ @param aComputedStyle the ComputedStyle of the element that the image is for, to support SVG
+ context paint properties, can be null
+ @param aResult the resulting NSImage
+ @param scaleFactor the desired scale factor of the NSImage (2 for a retina display)
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateNSImageFromImageContainer(imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext,
+ const mozilla::ComputedStyle* aComputedStyle,
+ NSImage** aResult, CGFloat scaleFactor,
+ bool* aIsEntirelyBlack = nullptr);
+
+ /** Creates a Cocoa <code>NSImage</code> from a frame of an <code>imgIContainer</code>.
+ The new <code>NSImage</code> will have both a regular and HiDPI representation.
+ The caller owns the <code>NSImage</code>.
+ @param aImage the image to extract a frame from
+ @param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
+ @param aComputedStyle the ComputedStyle of the element that the image is for, to support SVG
+ context paint properties, can be null
+ @param aResult the resulting NSImage
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateDualRepresentationNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame, const nsPresContext* aPresContext,
+ const mozilla::ComputedStyle* aComputedStyle, NSImage** aResult,
+ bool* aIsEntirelyBlack = nullptr);
+
+ /**
+ * Returns nsAString for aSrc.
+ */
+ static void GetStringForNSString(const NSString* aSrc, nsAString& aDist);
+
+ /**
+ * Makes NSString instance for aString.
+ */
+ static NSString* ToNSString(const nsAString& aString);
+
+ /**
+ * Returns an NSURL instance for the provided string.
+ */
+ static NSURL* ToNSURL(const nsAString& aURLString);
+
+ /**
+ * Makes NSString instance for aCString.
+ */
+ static NSString* ToNSString(const nsACString& aCString);
+
+ /**
+ * Returns NSRect for aGeckoRect.
+ * Just copies values between the two types; it does no coordinate-system
+ * conversion, so both rects must have the same coordinate origin/direction.
+ */
+ static void GeckoRectToNSRect(const nsIntRect& aGeckoRect, NSRect& aOutCocoaRect);
+
+ /**
+ * Returns Gecko rect for aCocoaRect.
+ * Just copies values between the two types; it does no coordinate-system
+ * conversion, so both rects must have the same coordinate origin/direction.
+ */
+ static void NSRectToGeckoRect(const NSRect& aCocoaRect, nsIntRect& aOutGeckoRect);
+
+ /**
+ * Makes NSEvent instance for aEventTytpe and aEvent.
+ */
+ static NSEvent* MakeNewCocoaEventWithType(NSEventType aEventType, NSEvent* aEvent);
+
+ /**
+ * Makes a cocoa event from a widget keyboard event.
+ */
+ static NSEvent* MakeNewCococaEventFromWidgetEvent(const mozilla::WidgetKeyboardEvent& aKeyEvent,
+ NSInteger aWindowNumber,
+ NSGraphicsContext* aContext);
+
+ /**
+ * Initializes WidgetInputEvent for aNativeEvent or aModifiers.
+ */
+ static void InitInputEvent(mozilla::WidgetInputEvent& aInputEvent, NSEvent* aNativeEvent);
+
+ /**
+ * Converts the native modifiers from aNativeEvent into WidgetMouseEvent
+ * Modifiers. aNativeEvent can be null.
+ */
+ static mozilla::Modifiers ModifiersForEvent(NSEvent* aNativeEvent);
+
+ /**
+ * ConvertToCarbonModifier() returns carbon modifier flags for the cocoa
+ * modifier flags.
+ * NOTE: The result never includes right*Key.
+ */
+ static UInt32 ConvertToCarbonModifier(NSUInteger aCocoaModifier);
+
+ /**
+ * Whether to support HiDPI rendering. For testing purposes, to be removed
+ * once we're comfortable with the HiDPI behavior.
+ */
+ static bool HiDPIEnabled();
+
+ /**
+ * Keys can optionally be bound by system or user key bindings to one or more
+ * commands based on selectors. This collects any such commands in the
+ * provided array.
+ */
+ static void GetCommandsFromKeyEvent(NSEvent* aEvent, nsTArray<KeyBindingsCommand>& aCommands);
+
+ /**
+ * Converts the string name of a Gecko key (like "VK_HOME") to the
+ * corresponding Cocoa Unicode character.
+ */
+ static uint32_t ConvertGeckoNameToMacCharCode(const nsAString& aKeyCodeName);
+
+ /**
+ * Converts a Gecko key code (like NS_VK_HOME) to the corresponding Cocoa
+ * Unicode character.
+ */
+ static uint32_t ConvertGeckoKeyCodeToMacCharCode(uint32_t aKeyCode);
+
+ /**
+ * Converts Gecko native modifier flags for `nsIWidget::SynthesizeNative*()`
+ * to native modifier flags of macOS.
+ */
+ static NSEventModifierFlags ConvertWidgetModifiersToMacModifierFlags(
+ nsIWidget::Modifiers aNativeModifiers);
+
+ /**
+ * Get the mouse button, which depends on the event's type and buttonNumber.
+ * Returns MouseButton::ePrimary for non-mouse events.
+ */
+ static mozilla::MouseButton ButtonForEvent(NSEvent* aEvent);
+
+ /**
+ * Convert string with font attribute to NSMutableAttributedString
+ */
+ static NSMutableAttributedString* GetNSMutableAttributedString(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRanges,
+ const bool aIsVertical, const CGFloat aBackingScaleFactor);
+
+ /**
+ * Compute TimeStamp from an event's timestamp.
+ * If aEventTime is 0, this returns current timestamp.
+ */
+ static mozilla::TimeStamp GetEventTimeStamp(NSTimeInterval aEventTime);
+
+ /**
+ * Check whether double clicking on the titlebar should cause the window to
+ * zoom (maximize).
+ */
+ static bool ShouldZoomOnTitlebarDoubleClick();
+
+ /**
+ * Check whether double clicking on the titlebar should cause the window to
+ * minimize.
+ */
+ static bool ShouldMinimizeOnTitlebarDoubleClick();
+
+ /**
+ * Get the current video capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult GetVideoCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Get the current audio capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult GetAudioCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Get the current screen capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.14 and earlier macOS versions.
+ */
+ static nsresult GetScreenCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Request video capture permission from the OS. Caller must be running
+ * on the main thread and the promise will be resolved on the main thread.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult RequestVideoCapturePermission(RefPtr<Promise>& aPromise);
+
+ /**
+ * Request audio capture permission from the OS. Caller must be running
+ * on the main thread and the promise will be resolved on the main thread.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult RequestAudioCapturePermission(RefPtr<Promise>& aPromise);
+
+ /**
+ * Request screen capture permission from the OS using an unreliable method.
+ */
+ static nsresult MaybeRequestScreenCapturePermission();
+
+ static void InvalidateHiDPIState();
+
+ static mozilla::PanGestureInput CreatePanGestureEvent(
+ NSEvent* aNativeEvent, mozilla::TimeStamp aTimeStamp,
+ const mozilla::ScreenPoint& aPanStartPoint, const mozilla::ScreenPoint& aPreciseDelta,
+ const mozilla::gfx::IntPoint& aLineOrPageDelta, mozilla::Modifiers aModifiers);
+
+ /**
+ * Return true if aAvailableType is a vaild NSPasteboard type.
+ */
+ static bool IsValidPasteboardType(NSString* aAvailableType, bool aAllowFileURL);
+
+ /**
+ * Set data for specific type from NSPasteboardItem to Transferable.
+ */
+ static void SetTransferDataForTypeFromPasteboardItem(nsITransferable* aTransferable,
+ const nsCString& aFlavor,
+ NSPasteboardItem* aItem);
+
+ private:
+ /**
+ * Completion handlers used as an argument to the macOS API to
+ * request media capture permission. These are called asynchronously
+ * on an arbitrary dispatch queue.
+ */
+ static void (^AudioCompletionHandler)(BOOL);
+ static void (^VideoCompletionHandler)(BOOL);
+
+ /**
+ * Called from the audio and video completion handlers in order to
+ * dispatch the handling back to the main thread.
+ */
+ static void ResolveAudioCapturePromises(bool aGranted);
+ static void ResolveVideoCapturePromises(bool aGranted);
+
+ /**
+ * Main implementation for Request{Audio,Video}CapturePermission.
+ * @param aType the AVMediaType to request capture permission for
+ * @param aPromise the Promise to resolve when capture permission
+ * is either allowed or denied
+ * @param aPromiseList the array of promises to save |aPromise| in
+ * @param aHandler the block function (either ResolveAudioCapturePromises
+ * or ResolveVideoCapturePromises) to be used as
+ * the requestAccessForMediaType callback.
+ */
+ static nsresult RequestCapturePermission(NSString* aType, RefPtr<Promise>& aPromise,
+ PromiseArray& aPromiseList,
+ void (^aHandler)(BOOL granted));
+ /**
+ * Resolves the pending promises that are waiting for a response
+ * to a request video or audio capture permission.
+ */
+ static void ResolveMediaCapturePromises(bool aGranted, PromiseArray& aPromiseList);
+
+ /**
+ * Get string data for a specific type from NSPasteboardItem.
+ */
+ static NSString* GetStringForTypeFromPasteboardItem(NSPasteboardItem* aItem,
+ const NSString* aType,
+ bool aAllowFileURL = false);
+
+ /**
+ * Get the file path from NSPasteboardItem.
+ */
+ static NSString* GetFilePathFromPasteboardItem(NSPasteboardItem* aItem);
+
+ /**
+ * Get the title for URL from NSPasteboardItem.
+ */
+ static NSString* GetTitleForURLFromPasteboardItem(NSPasteboardItem* item);
+
+ /**
+ * Did the OS launch the application at login?
+ */
+ static BOOL WasLaunchedAtLogin();
+
+ /**
+ * Should the application restore its state because it was launched by the OS
+ * at login?
+ */
+ static BOOL ShouldRestoreStateDueToLaunchAtLoginImpl();
+
+ /**
+ * Array of promises waiting to be resolved due to a video capture request.
+ */
+ static PromiseArray sVideoCapturePromises;
+
+ /**
+ * Array of promises waiting to be resolved due to an audio capture request.
+ */
+ static PromiseArray sAudioCapturePromises;
+
+ /**
+ * Lock protecting |sVideoCapturePromises| and |sAudioCapturePromises|.
+ */
+ static StaticMutex sMediaCaptureMutex MOZ_UNANNOTATED;
+};
+
+#endif // nsCocoaUtils_h_
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;
+}
diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
new file mode 100644
index 0000000000..5c57fcea55
--- /dev/null
+++ b/widget/cocoa/nsCocoaWindow.h
@@ -0,0 +1,493 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsCocoaWindow_h_
+#define nsCocoaWindow_h_
+
+#undef DARWIN
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/RefPtr.h"
+#include "nsBaseWidget.h"
+#include "nsPIWidgetCocoa.h"
+#include "nsCocoaUtils.h"
+#include "nsTouchBar.h"
+#include <dlfcn.h>
+#include <queue>
+
+class nsCocoaWindow;
+class nsChildView;
+class nsMenuBarX;
+@class ChildView;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+} // namespace mozilla
+
+typedef struct _nsCocoaWindowList {
+ _nsCocoaWindowList() : prev(nullptr), window(nullptr) {}
+ struct _nsCocoaWindowList* prev;
+ nsCocoaWindow* window; // Weak
+} nsCocoaWindowList;
+
+// NSWindow subclass that is the base class for all of our own window classes.
+// Among other things, this class handles the storage of those settings that
+// need to be persisted across window destruction and reconstruction, i.e. when
+// switching to and from fullscreen mode.
+// We don't save shadow, transparency mode or background color because it's not
+// worth the hassle - Gecko will reset them anyway as soon as the window is
+// resized.
+@interface BaseWindow : NSWindow {
+ // Data Storage
+ NSMutableDictionary* mState;
+ BOOL mDrawsIntoWindowFrame;
+
+ // Invalidation disabling
+ BOOL mDisabledNeedsDisplay;
+
+ NSTrackingArea* mTrackingArea;
+
+ NSRect mDirtyRect;
+
+ BOOL mBeingShown;
+ BOOL mDrawTitle;
+ BOOL mUseMenuStyle;
+ BOOL mIsAnimationSuppressed;
+
+ nsTouchBar* mTouchBar;
+}
+
+- (void)importState:(NSDictionary*)aState;
+- (NSMutableDictionary*)exportState;
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
+- (BOOL)drawsContentsIntoWindowFrame;
+
+// These two methods are like contentRectForFrameRect and frameRectForContentRect,
+// but they deal with the rect of the window's "main ChildView" instead of the
+// rect of the window's content view. The two are sometimes sized differently: The
+// window's content view always covers the entire window, whereas the ChildView
+// only covers the full window when drawsContentsIntoWindowFrame is YES. When
+// drawsContentsIntoWindowFrame is NO, there's a titlebar-sized gap above the
+// ChildView within the content view.
+- (NSRect)childViewRectForFrameRect:(NSRect)aFrameRect;
+- (NSRect)frameRectForChildViewRect:(NSRect)aChildViewRect;
+
+- (void)mouseEntered:(NSEvent*)aEvent;
+- (void)mouseExited:(NSEvent*)aEvent;
+- (void)mouseMoved:(NSEvent*)aEvent;
+- (void)updateTrackingArea;
+- (NSView*)trackingAreaView;
+
+- (void)setBeingShown:(BOOL)aValue;
+- (BOOL)isBeingShown;
+- (BOOL)isVisibleOrBeingShown;
+
+- (void)setIsAnimationSuppressed:(BOOL)aValue;
+- (BOOL)isAnimationSuppressed;
+
+// Returns an autoreleased NSArray containing the NSViews that we consider the
+// "contents" of this window. All views in the returned array are subviews of
+// this window's content view. However, the array may not include all of the
+// content view's subviews; concretely, the ToolbarWindow implementation will
+// exclude its MOZTitlebarView from the array that is returned here.
+// In the vast majority of cases, the array will only have a single element:
+// this window's mainChildView.
+- (NSArray<NSView*>*)contentViewContents;
+
+- (ChildView*)mainChildView;
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle;
+- (BOOL)wantsTitleDrawn;
+
+- (void)disableSetNeedsDisplay;
+- (void)enableSetNeedsDisplay;
+
+- (NSRect)getAndResetNativeDirtyRect;
+
+- (void)setUseMenuStyle:(BOOL)aValue;
+@property(nonatomic) mozilla::StyleWindowShadow shadowStyle;
+
+- (void)releaseJSObjects;
+
+@end
+
+@interface NSWindow (Undocumented)
+
+// If a window has been explicitly removed from the "window cache" (to
+// deactivate it), it's sometimes necessary to "reset" it to reactivate it
+// (and put it back in the "window cache"). One way to do this, which Apple
+// often uses, is to set the "window number" to '-1' and then back to its
+// original value.
+- (void)_setWindowNumber:(NSInteger)aNumber;
+
+- (BOOL)bottomCornerRounded;
+
+// Present in the same form on OS X since at least OS X 10.5.
+- (NSRect)contentRectForFrameRect:(NSRect)windowFrame styleMask:(NSUInteger)windowStyle;
+- (NSRect)frameRectForContentRect:(NSRect)windowContentRect styleMask:(NSUInteger)windowStyle;
+
+// Present since at least OS X 10.5. The OS calls this method on NSWindow
+// (and its subclasses) to find out which NSFrameView subclass to instantiate
+// to create its "frame view".
++ (Class)frameViewClassForStyleMask:(NSUInteger)styleMask;
+
+@end
+
+@interface PopupWindow : BaseWindow {
+ @private
+ BOOL mIsContextMenu;
+}
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)styleMask
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)deferCreation;
+- (BOOL)isContextMenu;
+- (void)setIsContextMenu:(BOOL)flag;
+- (BOOL)canBecomeMainWindow;
+
+@end
+
+@interface BorderlessWindow : BaseWindow {
+}
+
+- (BOOL)canBecomeKeyWindow;
+- (BOOL)canBecomeMainWindow;
+
+@end
+
+@interface WindowDelegate : NSObject <NSWindowDelegate> {
+ nsCocoaWindow* mGeckoWindow; // [WEAK] (we are owned by the window)
+ // Used to avoid duplication when we send NS_ACTIVATE and
+ // NS_DEACTIVATE to Gecko for toplevel widgets. Starts out
+ // false.
+ bool mToplevelActiveState;
+ BOOL mHasEverBeenZoomed;
+}
++ (void)paintMenubarForWindow:(NSWindow*)aWindow;
+- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind;
+- (void)windowDidResize:(NSNotification*)aNotification;
+- (nsCocoaWindow*)geckoWidget;
+- (bool)toplevelActiveState;
+- (void)sendToplevelActivateEvents;
+- (void)sendToplevelDeactivateEvents;
+@end
+
+@interface MOZTitlebarView : NSVisualEffectView
+@end
+
+@interface FullscreenTitlebarTracker : NSTitlebarAccessoryViewController
+- (FullscreenTitlebarTracker*)init;
+@end
+
+// NSWindow subclass for handling windows with toolbars.
+@interface ToolbarWindow : BaseWindow {
+ // This window's titlebar view, if present.
+ // Will be nil if the window has neither a titlebar nor a unified toolbar.
+ // This view is a subview of the window's content view and gets created and
+ // destroyed by updateTitlebarView.
+ MOZTitlebarView* mTitlebarView; // [STRONG]
+ // mFullscreenTitlebarTracker attaches an invisible rectangle to the system
+ // title bar. This allows us to detect when the title bar is showing in
+ // fullscreen.
+ FullscreenTitlebarTracker* mFullscreenTitlebarTracker;
+
+ CGFloat mUnifiedToolbarHeight;
+ CGFloat mSheetAttachmentPosition;
+ CGFloat mMenuBarHeight;
+ /* Store the height of the titlebar when this window is initialized. The
+ titlebarHeight getter returns 0 when in fullscreen, which is not useful in
+ some cases. */
+ CGFloat mInitialTitlebarHeight;
+ NSRect mWindowButtonsRect;
+}
+- (void)setUnifiedToolbarHeight:(CGFloat)aHeight;
+- (CGFloat)unifiedToolbarHeight;
+- (CGFloat)titlebarHeight;
+- (NSRect)titlebarRect;
+- (void)setTitlebarNeedsDisplay;
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
+- (void)setSheetAttachmentPosition:(CGFloat)aY;
+- (CGFloat)sheetAttachmentPosition;
+- (void)placeWindowButtons:(NSRect)aRect;
+- (NSRect)windowButtonsRect;
+- (void)windowMainStateChanged;
+@end
+
+class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa {
+ private:
+ typedef nsBaseWidget Inherited;
+
+ public:
+ nsCocoaWindow();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSPIWIDGETCOCOA; // semicolon for clang-format bug 1629756
+
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect, InitData* = nullptr) override;
+
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+ virtual bool NeedsRecreateToReshow() override;
+
+ virtual nsIWidget* GetSheetWindowParent(void) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetModal(bool aState) override;
+ virtual void SetFakeModal(bool aState) override;
+ virtual bool IsRunningAppModal() override;
+ virtual bool IsVisible() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual LayoutDeviceIntPoint GetClientOffset() override;
+ virtual LayoutDeviceIntMargin ClientToWindowMargin() override;
+
+ virtual void* GetNativeData(uint32_t aDataType) override;
+
+ virtual void ConstrainPosition(DesktopIntPoint&) override;
+ virtual void SetSizeConstraints(const SizeConstraints& aConstraints) override;
+ virtual void Move(double aX, double aY) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ virtual void GetWorkspaceID(nsAString& workspaceID) override;
+ virtual void MoveToWorkspace(const nsAString& workspaceID) override;
+ virtual void SuppressAnimation(bool aSuppress) override;
+ virtual void HideWindowChrome(bool aShouldHide) override;
+
+ virtual bool PrepareForFullscreenTransition(nsISupports** aData) override;
+ virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage, uint16_t aDuration,
+ nsISupports* aData, nsIRunnable* aCallback) override;
+ virtual void CleanupFullscreenTransition() override;
+ nsresult MakeFullScreen(bool aFullScreen) final;
+ nsresult MakeFullScreenWithNativeTransition(bool aFullScreen) final;
+ NSAnimation* FullscreenTransitionAnimation() const { return mFullscreenTransitionAnimation; }
+ void ReleaseFullscreenTransitionAnimation() {
+ MOZ_ASSERT(mFullscreenTransitionAnimation, "Should only be called when there is animation");
+ [mFullscreenTransitionAnimation release];
+ mFullscreenTransitionAnimation = nil;
+ }
+
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight, bool aRepaint) override;
+ NSRect GetClientCocoaRect();
+ virtual LayoutDeviceIntRect GetClientBounds() override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+ void ReportMoveEvent();
+ void ReportSizeEvent();
+ virtual void SetCursor(const Cursor&) override;
+
+ CGFloat BackingScaleFactor();
+ void BackingScaleFactorChanged();
+ virtual double GetDefaultScaleInternal() override;
+ virtual int32_t RoundsWidgetCoordinatesTo() override;
+
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() final {
+ return mozilla::DesktopToLayoutDeviceScale(BackingScaleFactor());
+ }
+
+ virtual nsresult SetTitle(const nsAString& aTitle) override;
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ virtual WindowRenderer* GetWindowRenderer() override;
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, nsEventStatus& aStatus) override;
+ virtual void CaptureRollupEvents(bool aDoCapture) override;
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override;
+ virtual bool HasPendingInputEvent() override;
+ virtual TransparencyMode GetTransparencyMode() override;
+ virtual void SetTransparencyMode(TransparencyMode aMode) override;
+ virtual void SetWindowShadowStyle(mozilla::StyleWindowShadow aStyle) override;
+ virtual void SetWindowOpacity(float aOpacity) override;
+ virtual void SetWindowTransform(const mozilla::gfx::Matrix& aTransform) override;
+ virtual void SetInputRegion(const InputRegion&) override;
+ virtual void SetColorScheme(const mozilla::Maybe<mozilla::ColorScheme>&) override;
+ virtual void SetShowsToolbarButton(bool aShow) override;
+ virtual void SetSupportsNativeFullscreen(bool aShow) override;
+ virtual void SetWindowAnimationType(WindowAnimationType aType) override;
+ virtual void SetDrawsTitle(bool aDrawTitle) override;
+ virtual nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override;
+ virtual void SetDrawsInTitlebar(bool aState) override;
+ virtual void UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) override;
+ virtual nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseScrollEvent(LayoutDeviceIntPoint aPoint,
+ uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ,
+ uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags,
+ nsIObserver* aObserver) override;
+ virtual void LockAspectRatio(bool aShouldLock) override;
+
+ void DispatchSizeModeEvent();
+ void DispatchOcclusionEvent();
+
+ // be notified that a some form of drag event needs to go into Gecko
+ virtual bool DragEvent(unsigned int aMessage, mozilla::gfx::Point aMouseGlobal,
+ UInt16 aKeyModifiers);
+
+ bool HasModalDescendents() { return mNumModalDescendents > 0; }
+ NSWindow* GetCocoaWindow() { return mWindow; }
+
+ void SetMenuBar(RefPtr<nsMenuBarX>&& aMenuBar);
+ nsMenuBarX* GetMenuBar();
+
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override { return mInputContext; }
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ void SetPopupWindowLevel();
+
+ bool InFullScreenMode() const { return mInFullScreenMode; }
+
+ void PauseCompositor();
+ void ResumeCompositor();
+
+ bool AsyncPanZoomEnabled() const override;
+
+ bool StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) override;
+ void StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ // Class method versions of NSWindow/Delegate callbacks which need to
+ // access object state.
+ void CocoaWindowWillEnterFullscreen(bool aFullscreen);
+ void CocoaWindowDidEnterFullscreen(bool aFullscreen);
+ void CocoaWindowDidFailFullscreen(bool aAttemptedFullscreen);
+ void CocoaWindowDidResize();
+ void CocoaSendToplevelActivateEvents();
+ void CocoaSendToplevelDeactivateEvents();
+
+ enum class TransitionType {
+ Windowed,
+ Fullscreen,
+ EmulatedFullscreen,
+ Miniaturize,
+ Deminiaturize,
+ Zoom,
+ };
+ void FinishCurrentTransition();
+ void FinishCurrentTransitionIfMatching(const TransitionType& aTransition);
+
+ // Called when something has happened that might cause us to update our
+ // fullscreen state. Returns true if we updated state. We'll call this
+ // on window resize, and we'll call it when we enter or exit fullscreen,
+ // since fullscreen to-and-from zoomed windows won't necessarily trigger
+ // a resize.
+ bool HandleUpdateFullscreenOnResize();
+
+ protected:
+ virtual ~nsCocoaWindow();
+
+ nsresult CreateNativeWindow(const NSRect& aRect, BorderStyle aBorderStyle, bool aRectIsFrameRect,
+ bool aIsPrivateBrowsing);
+ nsresult CreatePopupContentView(const LayoutDeviceIntRect& aRect, InitData*);
+ void DestroyNativeWindow();
+ void UpdateBounds();
+ int32_t GetWorkspaceID();
+
+ void DoResize(double aX, double aY, double aWidth, double aHeight, bool aRepaint,
+ bool aConstrainToCurrentScreen);
+
+ void UpdateFullscreenState(bool aFullScreen, bool aNativeMode);
+ nsresult DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition);
+
+ virtual already_AddRefed<nsIWidget> AllocateChildPopupWidget() override {
+ return nsIWidget::CreateTopLevelWindow();
+ }
+
+ nsIWidget* mParent; // if we're a popup, this is our parent [WEAK]
+ nsIWidget* mAncestorLink; // link to traverse ancestors [WEAK]
+ BaseWindow* mWindow; // our cocoa window [STRONG]
+ WindowDelegate* mDelegate; // our delegate for processing window msgs [STRONG]
+ RefPtr<nsMenuBarX> mMenuBar;
+ NSWindow* mSheetWindowParent; // if this is a sheet, this is the NSWindow it's attached to
+ nsChildView* mPopupContentView; // if this is a popup, this is its content widget
+ // if this is a toplevel window, and there is any ongoing fullscreen
+ // transition, it is the animation object.
+ NSAnimation* mFullscreenTransitionAnimation;
+ mozilla::StyleWindowShadow mShadowStyle;
+
+ CGFloat mBackingScaleFactor;
+ CGFloat mAspectRatio;
+
+ WindowAnimationType mAnimationType;
+
+ bool mWindowMadeHere; // true if we created the window, false for embedding
+ bool mSheetNeedsShow; // if this is a sheet, are we waiting to be shown?
+ // this is used for sibling sheet contention only
+ nsSizeMode mSizeMode;
+ bool mInFullScreenMode;
+ // Whether we are currently using native fullscreen. It could be false because
+ // we are in the emulated fullscreen where we do not use the native fullscreen.
+ bool mInNativeFullScreenMode;
+
+ mozilla::Maybe<TransitionType> mTransitionCurrent;
+ std::queue<TransitionType> mTransitionsPending;
+
+ // Sometimes we add a transition that wasn't requested by a caller. We do this
+ // to manage transitions between states that otherwise would be rejected by
+ // Cocoa. When we do this, it's useful to know when we are handling an added
+ // transition because we don't want to send size mode events when they execute.
+ bool mIsTransitionCurrentAdded = false;
+
+ // Whether we are treating the next resize as the start of a fullscreen transition.
+ // If we are, which direction are we going: Fullscreen or Windowed.
+ mozilla::Maybe<TransitionType> mUpdateFullscreenOnResize;
+
+ bool IsInTransition() { return mTransitionCurrent.isSome(); }
+ void QueueTransition(const TransitionType& aTransition);
+ void ProcessTransitions();
+
+ bool mInProcessTransitions = false;
+
+ // While running an emulated fullscreen transition, we want to suppress sending
+ // size mode events due to window resizing. We fix it up at the end when the
+ // transition is complete.
+ bool mSuppressSizeModeEvents = false;
+
+ // Ignore occlusion events caused by displaying the temporary fullscreen
+ // window during the fullscreen transition animation because only focused
+ // contexts are permitted to enter DOM fullscreen.
+ int mIgnoreOcclusionCount;
+
+ // Set to true when a native fullscreen transition is initiated -- either to
+ // or from fullscreen -- and set to false when it is complete. During this
+ // period, we presume the window is visible, which prevents us from sending
+ // unnecessary OcclusionStateChanged events.
+ bool mHasStartedNativeFullscreen;
+
+ bool mModal;
+ bool mFakeModal;
+
+ bool mIsAnimationSuppressed;
+
+ bool mInReportMoveEvent; // true if in a call to ReportMoveEvent().
+ bool mInResize; // true if in a call to DoResize().
+ bool mWindowTransformIsIdentity;
+ bool mAlwaysOnTop;
+ bool mAspectRatioLocked;
+
+ int32_t mNumModalDescendents;
+ InputContext mInputContext;
+ NSWindowAnimationBehavior mWindowAnimationBehavior;
+
+ private:
+ // true if Show() has been called.
+ bool mWasShown;
+};
+
+#endif // nsCocoaWindow_h_
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
new file mode 100644
index 0000000000..c3c246cc74
--- /dev/null
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -0,0 +1,4259 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsCocoaWindow.h"
+
+#include "AppearanceOverride.h"
+#include "NativeKeyBindings.h"
+#include "ScreenHelperCocoa.h"
+#include "TextInputHandler.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCOMPtr.h"
+#include "nsWidgetsCID.h"
+#include "nsIRollupListener.h"
+#include "nsChildView.h"
+#include "nsWindowMap.h"
+#include "nsAppShell.h"
+#include "nsIAppShellService.h"
+#include "nsIBaseWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAppWindow.h"
+#include "nsToolkit.h"
+#include "nsTouchBarNativeAPIDefines.h"
+#include "nsPIDOMWindow.h"
+#include "nsThreadUtils.h"
+#include "nsMenuBarX.h"
+#include "nsMenuUtilsX.h"
+#include "nsStyleConsts.h"
+#include "nsNativeThemeColors.h"
+#include "nsNativeThemeCocoa.h"
+#include "nsChildView.h"
+#include "nsCocoaFeatures.h"
+#include "nsIScreenManager.h"
+#include "nsIWidgetListener.h"
+#include "SDKDeclarations.h"
+#include "VibrancyManager.h"
+#include "nsPresContext.h"
+#include "nsDocShell.h"
+
+#include "gfxPlatform.h"
+#include "qcms.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/widget/Screen.h"
+#include <algorithm>
+
+namespace mozilla {
+namespace layers {
+class LayerManager;
+} // namespace layers
+} // namespace mozilla
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+using namespace mozilla;
+
+int32_t gXULModalLevel = 0;
+
+// In principle there should be only one app-modal window at any given time.
+// But sometimes, despite our best efforts, another window appears above the
+// current app-modal window. So we need to keep a linked list of app-modal
+// windows. (A non-sheet window that appears above an app-modal window is
+// also made app-modal.) See nsCocoaWindow::SetModal().
+nsCocoaWindowList* gGeckoAppModalWindowList = NULL;
+
+BOOL sTouchBarIsInitialized = NO;
+
+// defined in nsMenuBarX.mm
+extern NSMenu* sApplicationMenu; // Application menu shared by all menubars
+
+// defined in nsChildView.mm
+extern BOOL gSomeMenuBarPainted;
+
+extern "C" {
+// CGSPrivate.h
+typedef NSInteger CGSConnection;
+typedef NSUInteger CGSSpaceID;
+typedef NSInteger CGSWindow;
+typedef enum {
+ kCGSSpaceIncludesCurrent = 1 << 0,
+ kCGSSpaceIncludesOthers = 1 << 1,
+ kCGSSpaceIncludesUser = 1 << 2,
+
+ kCGSAllSpacesMask = kCGSSpaceIncludesCurrent | kCGSSpaceIncludesOthers | kCGSSpaceIncludesUser
+} CGSSpaceMask;
+static NSString* const CGSSpaceIDKey = @"ManagedSpaceID";
+static NSString* const CGSSpacesKey = @"Spaces";
+extern CGSConnection _CGSDefaultConnection(void);
+extern CGError CGSSetWindowTransform(CGSConnection cid, CGSWindow wid, CGAffineTransform transform);
+}
+
+#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
+
+NS_IMPL_ISUPPORTS_INHERITED(nsCocoaWindow, Inherited, nsPIWidgetCocoa)
+
+// A note on testing to see if your object is a sheet...
+// |mWindowType == WindowType::Sheet| is true if your gecko nsIWidget is a sheet
+// widget - whether or not the sheet is showing. |[mWindow isSheet]| will return
+// true *only when the sheet is actually showing*. Choose your test wisely.
+
+static void RollUpPopups(
+ nsIRollupListener::AllowAnimations aAllowAnimations = nsIRollupListener::AllowAnimations::Yes) {
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE_VOID(rollupListener);
+
+ if (rollupListener->RollupNativeMenu()) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return;
+ }
+ nsIRollupListener::RollupOptions options{0, nsIRollupListener::FlushViews::Yes, nullptr,
+ aAllowAnimations};
+ rollupListener->Rollup(options);
+}
+
+nsCocoaWindow::nsCocoaWindow()
+ : mParent(nullptr),
+ mAncestorLink(nullptr),
+ mWindow(nil),
+ mDelegate(nil),
+ mSheetWindowParent(nil),
+ mPopupContentView(nil),
+ mFullscreenTransitionAnimation(nil),
+ mShadowStyle(StyleWindowShadow::Default),
+ mBackingScaleFactor(0.0),
+ mAnimationType(nsIWidget::eGenericWindowAnimation),
+ mWindowMadeHere(false),
+ mSheetNeedsShow(false),
+ mSizeMode(nsSizeMode_Normal),
+ mInFullScreenMode(false),
+ mInNativeFullScreenMode(false),
+ mIgnoreOcclusionCount(0),
+ mHasStartedNativeFullscreen(false),
+ mModal(false),
+ mFakeModal(false),
+ mIsAnimationSuppressed(false),
+ mInReportMoveEvent(false),
+ mInResize(false),
+ mWindowTransformIsIdentity(true),
+ mAlwaysOnTop(false),
+ mAspectRatioLocked(false),
+ mNumModalDescendents(0),
+ mWindowAnimationBehavior(NSWindowAnimationBehaviorDefault),
+ mWasShown(false) {
+ // Disable automatic tabbing. We need to do this before we
+ // orderFront any of our windows.
+ [NSWindow setAllowsAutomaticWindowTabbing:NO];
+}
+
+void nsCocoaWindow::DestroyNativeWindow() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+
+ [mWindow releaseJSObjects];
+ // We want to unhook the delegate here because we don't want events
+ // sent to it after this object has been destroyed.
+ [mWindow setDelegate:nil];
+ [mWindow close];
+ mWindow = nil;
+ [mDelegate autorelease];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsCocoaWindow::~nsCocoaWindow() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Notify the children that we're gone. Popup windows (e.g. tooltips) can
+ // have nsChildView children. 'kid' is an nsChildView object if and only if
+ // its 'type' is 'WindowType::Child'.
+ // childView->ResetParent() can change our list of children while it's
+ // being iterated, so the way we iterate the list must allow for this.
+ for (nsIWidget* kid = mLastChild; kid;) {
+ WindowType kidType = kid->GetWindowType();
+ if (kidType == WindowType::Child) {
+ nsChildView* childView = static_cast<nsChildView*>(kid);
+ kid = kid->GetPrevSibling();
+ childView->ResetParent();
+ } else {
+ nsCocoaWindow* childWindow = static_cast<nsCocoaWindow*>(kid);
+ childWindow->mParent = nullptr;
+ childWindow->mAncestorLink = mAncestorLink;
+ kid = kid->GetPrevSibling();
+ }
+ }
+
+ if (mWindow && mWindowMadeHere) {
+ DestroyNativeWindow();
+ }
+
+ NS_IF_RELEASE(mPopupContentView);
+
+ // Deal with the possiblity that we're being destroyed while running modal.
+ if (mModal) {
+ NS_WARNING("Widget destroyed while running modal!");
+ --gXULModalLevel;
+ NS_ASSERTION(gXULModalLevel >= 0, "Weirdness setting modality!");
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Find the screen that overlaps aRect the most,
+// if none are found default to the mainScreen.
+static NSScreen* FindTargetScreenForRect(const DesktopIntRect& aRect) {
+ NSScreen* targetScreen = [NSScreen mainScreen];
+ NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
+ int largestIntersectArea = 0;
+ while (NSScreen* screen = [screenEnum nextObject]) {
+ DesktopIntRect screenRect = nsCocoaUtils::CocoaRectToGeckoRect([screen visibleFrame]);
+ screenRect = screenRect.Intersect(aRect);
+ int area = screenRect.width * screenRect.height;
+ if (area > largestIntersectArea) {
+ largestIntersectArea = area;
+ targetScreen = screen;
+ }
+ }
+ return targetScreen;
+}
+
+// fits the rect to the screen that contains the largest area of it,
+// or to aScreen if a screen is passed in
+// NB: this operates with aRect in desktop pixels
+static void FitRectToVisibleAreaForScreen(DesktopIntRect& aRect, NSScreen* aScreen) {
+ if (!aScreen) {
+ aScreen = FindTargetScreenForRect(aRect);
+ }
+
+ DesktopIntRect screenBounds = nsCocoaUtils::CocoaRectToGeckoRect([aScreen visibleFrame]);
+
+ if (aRect.width > screenBounds.width) {
+ aRect.width = screenBounds.width;
+ }
+ if (aRect.height > screenBounds.height) {
+ aRect.height = screenBounds.height;
+ }
+
+ if (aRect.x - screenBounds.x + aRect.width > screenBounds.width) {
+ aRect.x += screenBounds.width - (aRect.x - screenBounds.x + aRect.width);
+ }
+ if (aRect.y - screenBounds.y + aRect.height > screenBounds.height) {
+ aRect.y += screenBounds.height - (aRect.y - screenBounds.y + aRect.height);
+ }
+
+ // If the left/top edge of the window is off the screen in either direction,
+ // then set the window to start at the left/top edge of the screen.
+ if (aRect.x < screenBounds.x || aRect.x > (screenBounds.x + screenBounds.width)) {
+ aRect.x = screenBounds.x;
+ }
+ if (aRect.y < screenBounds.y || aRect.y > (screenBounds.y + screenBounds.height)) {
+ aRect.y = screenBounds.y;
+ }
+}
+
+DesktopToLayoutDeviceScale ParentBackingScaleFactor(nsIWidget* aParent, NSView* aParentView) {
+ if (aParent) {
+ return aParent->GetDesktopToDeviceScale();
+ }
+ NSWindow* parentWindow = [aParentView window];
+ if (parentWindow) {
+ return DesktopToLayoutDeviceScale([parentWindow backingScaleFactor]);
+ }
+ return DesktopToLayoutDeviceScale(1.0);
+}
+
+// Returns the screen rectangle for the given widget.
+// Child widgets are positioned relative to this rectangle.
+// Exactly one of the arguments must be non-null.
+static DesktopRect GetWidgetScreenRectForChildren(nsIWidget* aWidget, NSView* aView) {
+ if (aWidget) {
+ mozilla::DesktopToLayoutDeviceScale scale = aWidget->GetDesktopToDeviceScale();
+ if (aWidget->GetWindowType() == WindowType::Child) {
+ return aWidget->GetScreenBounds() / scale;
+ }
+ return aWidget->GetClientBounds() / scale;
+ }
+
+ MOZ_RELEASE_ASSERT(aView);
+
+ // 1. Transform the view rect into window coords.
+ // The returned rect is in "origin bottom-left" coordinates.
+ NSRect rectInWindowCoordinatesOBL = [aView convertRect:[aView bounds] toView:nil];
+
+ // 2. Turn the window-coord rect into screen coords, still origin bottom-left.
+ NSRect rectInScreenCoordinatesOBL =
+ [[aView window] convertRectToScreen:rectInWindowCoordinatesOBL];
+
+ // 3. Convert the NSRect to a DesktopRect. This will convert to coordinates
+ // with the origin in the top left corner of the primary screen.
+ return DesktopRect(nsCocoaUtils::CocoaRectToGeckoRect(rectInScreenCoordinatesOBL));
+}
+
+// aRect here is specified in desktop pixels
+//
+// For child windows (where either aParent or aNativeParent is non-null),
+// aRect.{x,y} are offsets from the origin of the parent window and not an
+// absolute position.
+nsresult nsCocoaWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect, widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Because the hidden window is created outside of an event loop,
+ // we have to provide an autorelease pool (see bug 559075).
+ nsAutoreleasePool localPool;
+
+ DesktopIntRect newBounds = aRect;
+ FitRectToVisibleAreaForScreen(newBounds, nullptr);
+
+ // Set defaults which can be overriden from aInitData in BaseCreate
+ mWindowType = WindowType::TopLevel;
+ mBorderStyle = BorderStyle::Default;
+
+ // Ensure that the toolkit is created.
+ nsToolkit::GetToolkit();
+
+ Inherited::BaseCreate(aParent, aInitData);
+
+ mParent = aParent;
+ mAncestorLink = aParent;
+ mAlwaysOnTop = aInitData->mAlwaysOnTop;
+
+ // If we have a parent widget, the new widget will be offset from the
+ // parent widget by aRect.{x,y}. Otherwise, we'll use aRect for the
+ // new widget coordinates.
+ DesktopIntPoint parentOrigin;
+
+ // Do we have a parent widget?
+ if (aParent || aNativeParent) {
+ DesktopRect parentDesktopRect = GetWidgetScreenRectForChildren(aParent, (NSView*)aNativeParent);
+ parentOrigin = gfx::RoundedToInt(parentDesktopRect.TopLeft());
+ }
+
+ DesktopIntRect widgetRect = aRect + parentOrigin;
+
+ nsresult rv = CreateNativeWindow(nsCocoaUtils::GeckoRectToCocoaRect(widgetRect), mBorderStyle,
+ false, aInitData->mIsPrivate);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mWindowType == WindowType::Popup) {
+ // now we can convert widgetRect to device pixels for the window we created,
+ // as the child view expects a rect expressed in the dev pix of its parent
+ LayoutDeviceIntRect devRect = RoundedToInt(newBounds * GetDesktopToDeviceScale());
+ return CreatePopupContentView(devRect, aInitData);
+ }
+
+ mIsAnimationSuppressed = aInitData->mIsAnimationSuppressed;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData) {
+ DesktopIntRect desktopRect =
+ RoundedToInt(aRect / ParentBackingScaleFactor(aParent, (NSView*)aNativeParent));
+ return Create(aParent, aNativeParent, desktopRect, aInitData);
+}
+
+static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
+ bool allOrDefault = (aBorderStyle == BorderStyle::All || aBorderStyle == BorderStyle::Default);
+
+ /* Apple's docs on NSWindow styles say that "a window's style mask should
+ * include NSWindowStyleMaskTitled if it includes any of the others [besides
+ * NSWindowStyleMaskBorderless]". This implies that a borderless window
+ * shouldn't have any other styles than NSWindowStyleMaskBorderless.
+ */
+ if (!allOrDefault && !(aBorderStyle & BorderStyle::Title)) {
+ if (aBorderStyle & BorderStyle::Minimize) {
+ /* It appears that at a minimum, borderless windows can be miniaturizable,
+ * effectively contradicting some of Apple's documentation referenced
+ * above. One such exception is the screen share indicator, see
+ * bug 1742877.
+ */
+ return NSWindowStyleMaskBorderless | NSWindowStyleMaskMiniaturizable;
+ }
+ return NSWindowStyleMaskBorderless;
+ }
+
+ unsigned int mask = NSWindowStyleMaskTitled;
+ if (allOrDefault || aBorderStyle & BorderStyle::Close) {
+ mask |= NSWindowStyleMaskClosable;
+ }
+ if (allOrDefault || aBorderStyle & BorderStyle::Minimize) {
+ mask |= NSWindowStyleMaskMiniaturizable;
+ }
+ if (allOrDefault || aBorderStyle & BorderStyle::ResizeH) {
+ mask |= NSWindowStyleMaskResizable;
+ }
+
+ return mask;
+}
+
+// If aRectIsFrameRect, aRect specifies the frame rect of the new window.
+// Otherwise, aRect.x/y specify the position of the window's frame relative to
+// the bottom of the menubar and aRect.width/height specify the size of the
+// content rect.
+nsresult nsCocoaWindow::CreateNativeWindow(const NSRect& aRect, BorderStyle aBorderStyle,
+ bool aRectIsFrameRect, bool aIsPrivateBrowsing) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We default to NSWindowStyleMaskBorderless, add features if needed.
+ unsigned int features = NSWindowStyleMaskBorderless;
+
+ // Configure the window we will create based on the window type.
+ switch (mWindowType) {
+ case WindowType::Invisible:
+ case WindowType::Child:
+ break;
+ case WindowType::Popup:
+ if (aBorderStyle != BorderStyle::Default && mBorderStyle & BorderStyle::Title) {
+ features |= NSWindowStyleMaskTitled;
+ if (aBorderStyle & BorderStyle::Close) {
+ features |= NSWindowStyleMaskClosable;
+ }
+ }
+ break;
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ features = WindowMaskForBorderStyle(aBorderStyle);
+ break;
+ case WindowType::Sheet:
+ if (mParent->GetWindowType() != WindowType::Invisible &&
+ aBorderStyle & BorderStyle::ResizeH) {
+ features = NSWindowStyleMaskResizable;
+ } else {
+ features = NSWindowStyleMaskMiniaturizable;
+ }
+ features |= NSWindowStyleMaskTitled;
+ break;
+ default:
+ NS_ERROR("Unhandled window type!");
+ return NS_ERROR_FAILURE;
+ }
+
+ NSRect contentRect;
+
+ if (aRectIsFrameRect) {
+ contentRect = [NSWindow contentRectForFrameRect:aRect styleMask:features];
+ } else {
+ /*
+ * We pass a content area rect to initialize the native Cocoa window. The
+ * content rect we give is the same size as the size we're given by gecko.
+ * The origin we're given for non-popup windows is moved down by the height
+ * of the menu bar so that an origin of (0,100) from gecko puts the window
+ * 100 pixels below the top of the available desktop area. We also move the
+ * origin down by the height of a title bar if it exists. This is so the
+ * origin that gecko gives us for the top-left of the window turns out to
+ * be the top-left of the window we create. This is how it was done in
+ * Carbon. If it ought to be different we'll probably need to look at all
+ * the callers.
+ *
+ * Note: This means that if you put a secondary screen on top of your main
+ * screen and open a window in the top screen, it'll be incorrectly shifted
+ * down by the height of the menu bar. Same thing would happen in Carbon.
+ *
+ * Note: If you pass a rect with 0,0 for an origin, the window ends up in a
+ * weird place for some reason. This stops that without breaking popups.
+ */
+ // Compensate for difference between frame and content area height (e.g. title bar).
+ NSRect newWindowFrame = [NSWindow frameRectForContentRect:aRect styleMask:features];
+
+ contentRect = aRect;
+ contentRect.origin.y -= (newWindowFrame.size.height - aRect.size.height);
+
+ if (mWindowType != WindowType::Popup) contentRect.origin.y -= [[NSApp mainMenu] menuBarHeight];
+ }
+
+ // NSLog(@"Top-level window being created at Cocoa rect: %f, %f, %f, %f\n",
+ // rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+
+ Class windowClass = [BaseWindow class];
+ // If we have a titlebar on a top-level window, we want to be able to control the
+ // titlebar color (for unified windows), so use the special ToolbarWindow class.
+ // Note that we need to check the window type because we mark sheets as
+ // having titlebars.
+ if ((mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) &&
+ (features & NSWindowStyleMaskTitled))
+ windowClass = [ToolbarWindow class];
+ // If we're a popup window we need to use the PopupWindow class.
+ else if (mWindowType == WindowType::Popup)
+ windowClass = [PopupWindow class];
+ // If we're a non-popup borderless window we need to use the
+ // BorderlessWindow class.
+ else if (features == NSWindowStyleMaskBorderless)
+ windowClass = [BorderlessWindow class];
+
+ // Create the window
+ mWindow = [[windowClass alloc] initWithContentRect:contentRect
+ styleMask:features
+ backing:NSBackingStoreBuffered
+ defer:YES];
+
+ // Make sure that window titles don't leak to disk in private browsing mode
+ // due to macOS' resume feature.
+ [mWindow setRestorable:!aIsPrivateBrowsing];
+ if (aIsPrivateBrowsing) {
+ [mWindow disableSnapshotRestoration];
+ }
+
+ // setup our notification delegate. Note that setDelegate: does NOT retain.
+ mDelegate = [[WindowDelegate alloc] initWithGeckoWindow:this];
+ [mWindow setDelegate:mDelegate];
+
+ // Make sure that the content rect we gave has been honored.
+ NSRect wantedFrame = [mWindow frameRectForChildViewRect:contentRect];
+ if (!NSEqualRects([mWindow frame], wantedFrame)) {
+ // This can happen when the window is not on the primary screen.
+ [mWindow setFrame:wantedFrame display:NO];
+ }
+ UpdateBounds();
+
+ if (mWindowType == WindowType::Invisible) {
+ [mWindow setLevel:kCGDesktopWindowLevelKey];
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ SetPopupWindowLevel();
+ [mWindow setBackgroundColor:[NSColor clearColor]];
+ [mWindow setOpaque:NO];
+
+ // When multiple spaces are in use and the browser is assigned to a
+ // particular space, override the "Assign To" space and display popups on
+ // the active space. Does not work with multiple displays. See
+ // NeedsRecreateToReshow() for multi-display with multi-space workaround.
+ if (!mAlwaysOnTop) {
+ NSWindowCollectionBehavior behavior = [mWindow collectionBehavior];
+ behavior |= NSWindowCollectionBehaviorMoveToActiveSpace;
+ [mWindow setCollectionBehavior:behavior];
+ }
+ } else {
+ // Non-popup windows are always opaque.
+ [mWindow setOpaque:YES];
+ }
+
+ NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior];
+ if (mAlwaysOnTop) {
+ [mWindow setLevel:NSFloatingWindowLevel];
+ newBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
+ }
+ [mWindow setCollectionBehavior:newBehavior];
+
+ [mWindow setContentMinSize:NSMakeSize(60, 60)];
+ [mWindow disableCursorRects];
+
+ // Make the window use CoreAnimation from the start, so that we don't
+ // switch from a non-CA window to a CA-window in the middle.
+ [[mWindow contentView] setWantsLayer:YES];
+
+ // Make sure the window starts out not draggable by the background.
+ // We will turn it on as necessary.
+ [mWindow setMovableByWindowBackground:NO];
+
+ [[WindowDataMap sharedWindowDataMap] ensureDataForWindow:mWindow];
+ mWindowMadeHere = true;
+
+ if (@available(macOS 10.14, *)) {
+ // Make the window respect the global appearance, which follows the browser.theme.toolbar-theme
+ // pref.
+ mWindow.appearanceSource = MOZGlobalAppearance.sharedInstance;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaWindow::CreatePopupContentView(const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We need to make our content view a ChildView.
+ mPopupContentView = new nsChildView();
+ if (!mPopupContentView) return NS_ERROR_FAILURE;
+
+ NS_ADDREF(mPopupContentView);
+
+ nsIWidget* thisAsWidget = static_cast<nsIWidget*>(this);
+ nsresult rv = mPopupContentView->Create(thisAsWidget, nullptr, aRect, aInitData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ NSView* contentView = [mWindow contentView];
+ ChildView* childView = (ChildView*)mPopupContentView->GetNativeData(NS_NATIVE_WIDGET);
+ [childView setFrame:[contentView bounds]];
+ [childView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ [contentView addSubview:childView];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::Destroy() {
+ if (mOnDestroyCalled) return;
+ mOnDestroyCalled = true;
+
+ // SetFakeModal(true) is called for non-modal window opened by modal window.
+ // On Cocoa, it needs corresponding SetFakeModal(false) on destroy to restore
+ // ancestor windows' state.
+ if (mFakeModal) {
+ SetFakeModal(false);
+ }
+
+ // If we don't hide here we run into problems with panels, this is not ideal.
+ // (Bug 891424)
+ Show(false);
+
+ if (mPopupContentView) mPopupContentView->Destroy();
+
+ if (mFullscreenTransitionAnimation) {
+ [mFullscreenTransitionAnimation stopAnimation];
+ ReleaseFullscreenTransitionAnimation();
+ }
+
+ nsBaseWidget::Destroy();
+ // nsBaseWidget::Destroy() calls GetParent()->RemoveChild(this). But we
+ // don't implement GetParent(), so we need to do the equivalent here.
+ if (mParent) {
+ mParent->RemoveChild(this);
+ }
+ nsBaseWidget::OnDestroy();
+
+ if (mInFullScreenMode) {
+ // On Lion we don't have to mess with the OS chrome when in Full Screen
+ // mode. But we do have to destroy the native window here (and not wait
+ // for that to happen in our destructor). We don't switch away from the
+ // native window's space until the window is destroyed, and otherwise this
+ // might not happen for several seconds (because at least one object
+ // holding a reference to ourselves is usually waiting to be garbage-
+ // collected). See bug 757618.
+ if (mInNativeFullScreenMode) {
+ DestroyNativeWindow();
+ } else if (mWindow) {
+ nsCocoaUtils::HideOSChromeOnScreen(false);
+ }
+ }
+}
+
+nsIWidget* nsCocoaWindow::GetSheetWindowParent(void) {
+ if (mWindowType != WindowType::Sheet) return nullptr;
+ nsCocoaWindow* parent = static_cast<nsCocoaWindow*>(mParent);
+ while (parent && (parent->mWindowType == WindowType::Sheet))
+ parent = static_cast<nsCocoaWindow*>(parent->mParent);
+ return parent;
+}
+
+void* nsCocoaWindow::GetNativeData(uint32_t aDataType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ // to emulate how windows works, we always have to return a NSView
+ // for NS_NATIVE_WIDGET
+ case NS_NATIVE_WIDGET:
+ retVal = [mWindow contentView];
+ break;
+
+ case NS_NATIVE_WINDOW:
+ retVal = mWindow;
+ break;
+
+ case NS_NATIVE_GRAPHIC:
+ // There isn't anything that makes sense to return here,
+ // and it doesn't matter so just return nullptr.
+ NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a top-level window!");
+ break;
+ case NS_RAW_NATIVE_IME_CONTEXT: {
+ retVal = GetPseudoIMEContext();
+ if (retVal) {
+ break;
+ }
+ NSView* view = mWindow ? [mWindow contentView] : nil;
+ if (view) {
+ retVal = [view inputContext];
+ }
+ // If inputContext isn't available on this window, return this window's
+ // pointer instead of nullptr since if this returns nullptr,
+ // IMEStateManager cannot manage composition with TextComposition
+ // instance. Although, this case shouldn't occur.
+ if (NS_WARN_IF(!retVal)) {
+ retVal = this;
+ }
+ break;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+bool nsCocoaWindow::IsVisible() const {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (mWindow && ([mWindow isVisibleOrBeingShown] || mSheetNeedsShow));
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void nsCocoaWindow::SetModal(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+
+ // This is used during startup (outside the event loop) when creating
+ // the add-ons compatibility checking dialog and the profile manager UI;
+ // therefore, it needs to provide an autorelease pool to avoid cocoa
+ // objects leaking.
+ nsAutoreleasePool localPool;
+
+ mModal = aState;
+ nsCocoaWindow* ancestor = static_cast<nsCocoaWindow*>(mAncestorLink);
+ if (aState) {
+ ++gXULModalLevel;
+ // When a non-sheet window gets "set modal", make the window(s) that it
+ // appears over behave as they should. We can't rely on native methods to
+ // do this, for the following reason: The OS runs modal non-sheet windows
+ // in an event loop (using [NSApplication runModalForWindow:] or similar
+ // methods) that's incompatible with the modal event loop in AppWindow::
+ // ShowModal() (each of these event loops is "exclusive", and can't run at
+ // the same time as other (similar) event loops).
+ if (mWindowType != WindowType::Sheet) {
+ while (ancestor) {
+ if (ancestor->mNumModalDescendents++ == 0) {
+ NSWindow* aWindow = ancestor->GetCocoaWindow();
+ if (ancestor->mWindowType != WindowType::Invisible) {
+ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:NO];
+ [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO];
+ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:NO];
+ }
+ }
+ ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent);
+ }
+ [mWindow setLevel:NSModalPanelWindowLevel];
+ nsCocoaWindowList* windowList = new nsCocoaWindowList;
+ if (windowList) {
+ windowList->window = this; // Don't ADDREF
+ windowList->prev = gGeckoAppModalWindowList;
+ gGeckoAppModalWindowList = windowList;
+ }
+ }
+ } else {
+ --gXULModalLevel;
+ NS_ASSERTION(gXULModalLevel >= 0, "Mismatched call to nsCocoaWindow::SetModal(false)!");
+ if (mWindowType != WindowType::Sheet) {
+ while (ancestor) {
+ if (--ancestor->mNumModalDescendents == 0) {
+ NSWindow* aWindow = ancestor->GetCocoaWindow();
+ if (ancestor->mWindowType != WindowType::Invisible) {
+ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:YES];
+ [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:YES];
+ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:YES];
+ }
+ }
+ NS_ASSERTION(ancestor->mNumModalDescendents >= 0, "Widget hierarchy changed while modal!");
+ ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent);
+ }
+ if (gGeckoAppModalWindowList) {
+ NS_ASSERTION(gGeckoAppModalWindowList->window == this,
+ "Widget hierarchy changed while modal!");
+ nsCocoaWindowList* saved = gGeckoAppModalWindowList;
+ gGeckoAppModalWindowList = gGeckoAppModalWindowList->prev;
+ delete saved; // "window" not ADDREFed
+ }
+ if (mWindowType == WindowType::Popup)
+ SetPopupWindowLevel();
+ else
+ [mWindow setLevel:NSNormalWindowLevel];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetFakeModal(bool aState) {
+ mFakeModal = aState;
+ SetModal(aState);
+}
+
+bool nsCocoaWindow::IsRunningAppModal() { return [NSApp _isRunningAppModal]; }
+
+// Hide or show this window
+void nsCocoaWindow::Show(bool bState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+
+ if (!mSheetNeedsShow) {
+ // Early exit if our current visibility state is already the requested state.
+ if (bState == ([mWindow isVisible] || [mWindow isBeingShown])) {
+ return;
+ }
+ }
+
+ [mWindow setBeingShown:bState];
+ if (bState && !mWasShown) {
+ mWasShown = true;
+ }
+
+ nsIWidget* parentWidget = mParent;
+ nsCOMPtr<nsPIWidgetCocoa> piParentWidget(do_QueryInterface(parentWidget));
+ NSWindow* nativeParentWindow =
+ (parentWidget) ? (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW) : nil;
+
+ if (bState && !mBounds.IsEmpty()) {
+ // If we had set the activationPolicy to accessory, then right now we won't
+ // have a dock icon. Make sure that we undo that and show a dock icon now that
+ // we're going to show a window.
+ if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
+ PR_SetEnv("MOZ_APP_NO_DOCK=");
+ }
+
+ // Don't try to show a popup when the parent isn't visible or is minimized.
+ if (mWindowType == WindowType::Popup && nativeParentWindow) {
+ if (![nativeParentWindow isVisible] || [nativeParentWindow isMiniaturized]) {
+ return;
+ }
+ }
+
+ if (mPopupContentView) {
+ // Ensure our content view is visible. We never need to hide it.
+ mPopupContentView->Show(true);
+ }
+
+ if (mWindowType == WindowType::Sheet) {
+ // bail if no parent window (its basically what we do in Carbon)
+ if (!nativeParentWindow || !piParentWidget) return;
+
+ NSWindow* topNonSheetWindow = nativeParentWindow;
+
+ // If this sheet is the child of another sheet, hide the parent so that
+ // this sheet can be displayed. Leave the parent mSheetNeedsShow alone,
+ // that is only used to handle sibling sheet contention. The parent will
+ // return once there are no more child sheets.
+ bool parentIsSheet = false;
+ if (NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && parentIsSheet) {
+ piParentWidget->GetSheetWindowParent(&topNonSheetWindow);
+#ifdef MOZ_THUNDERBIRD
+ [NSApp endSheet:nativeParentWindow];
+#else
+ [nativeParentWindow.sheetParent endSheet:nativeParentWindow];
+#endif
+ }
+
+ nsCOMPtr<nsIWidget> sheetShown;
+ if (NS_SUCCEEDED(piParentWidget->GetChildSheet(true, getter_AddRefs(sheetShown))) &&
+ (!sheetShown || sheetShown == this)) {
+ // If this sheet is already the sheet actually being shown, don't
+ // tell it to show again. Otherwise the number of calls to
+#ifdef MOZ_THUNDERBIRD
+ // [NSApp beginSheet...] won't match up with [NSApp endSheet...].
+#else
+ // [NSWindow beginSheet...] won't match up with [NSWindow endSheet...].
+#endif
+ if (![mWindow isVisible]) {
+ mSheetNeedsShow = false;
+ mSheetWindowParent = topNonSheetWindow;
+#ifdef MOZ_THUNDERBIRD
+ // Only set contextInfo if our parent isn't a sheet.
+ NSWindow* contextInfo = parentIsSheet ? nil : mSheetWindowParent;
+ [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+ [NSApp beginSheet:mWindow
+ modalForWindow:mSheetWindowParent
+ modalDelegate:mDelegate
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:contextInfo];
+#else
+ NSWindow* sheet = mWindow;
+ NSWindow* nonSheetParent = parentIsSheet ? nil : mSheetWindowParent;
+ [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+ [mSheetWindowParent beginSheet:sheet
+ completionHandler:^(NSModalResponse returnCode) {
+ // Note: 'nonSheetParent' (if it is set) is the window that is the parent
+ // of the sheet. If it's set, 'nonSheetParent' is always the top- level
+ // window, not another sheet itself. But 'nonSheetParent' is nil if our
+ // parent window is also a sheet -- in that case we shouldn't send the
+ // top-level window any activate events (because it's our parent window
+ // that needs to get these events, not the top-level window).
+ [TopLevelWindowData deactivateInWindow:sheet];
+ [sheet orderOut:nil];
+ if (nonSheetParent) {
+ [TopLevelWindowData activateInWindow:nonSheetParent];
+ }
+ }];
+#endif
+ [TopLevelWindowData activateInWindow:mWindow];
+ SendSetZLevelEvent();
+ }
+ } else {
+ // A sibling of this sheet is active, don't show this sheet yet.
+ // When the active sheet hides, its brothers and sisters that have
+ // mSheetNeedsShow set will have their opportunities to display.
+ mSheetNeedsShow = true;
+ }
+ } else if (mWindowType == WindowType::Popup) {
+ // For reasons that aren't yet clear, calls to [NSWindow orderFront:] or
+ // [NSWindow makeKeyAndOrderFront:] can sometimes trigger "Error (1000)
+ // creating CGSWindow", which in turn triggers an internal inconsistency
+ // NSException. These errors shouldn't be fatal. So we need to wrap
+ // calls to ...orderFront: in TRY blocks. See bmo bug 470864.
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [[mWindow contentView] setNeedsDisplay:YES];
+ [mWindow orderFront:nil];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ SendSetZLevelEvent();
+ // If our popup window is a non-native context menu, tell the OS (and
+ // other programs) that a menu has opened. This is how the OS knows to
+ // close other programs' context menus when ours open.
+ if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) {
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.HIToolbox.beginMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ }
+
+ // If a parent window was supplied and this is a popup at the parent
+ // level, set its child window. This will cause the child window to
+ // appear above the parent and move when the parent does. Setting this
+ // needs to happen after the _setWindowNumber calls above, otherwise the
+ // window doesn't focus properly.
+ if (nativeParentWindow && mPopupLevel == PopupLevel::Parent)
+ [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
+ } else {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ if (mWindowType == WindowType::TopLevel &&
+ [mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
+ NSWindowAnimationBehavior behavior;
+ if (mIsAnimationSuppressed) {
+ behavior = NSWindowAnimationBehaviorNone;
+ } else {
+ switch (mAnimationType) {
+ case nsIWidget::eDocumentWindowAnimation:
+ behavior = NSWindowAnimationBehaviorDocumentWindow;
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("unexpected mAnimationType value");
+ case nsIWidget::eGenericWindowAnimation:
+ behavior = NSWindowAnimationBehaviorDefault;
+ break;
+ }
+ }
+ [mWindow setAnimationBehavior:behavior];
+ mWindowAnimationBehavior = behavior;
+ }
+
+ // We don't want alwaysontop windows to pull focus when they're opened,
+ // as these tend to be for peripheral indicators and displays.
+ if (mAlwaysOnTop) {
+ [mWindow orderFront:nil];
+ } else {
+ [mWindow makeKeyAndOrderFront:nil];
+ }
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ SendSetZLevelEvent();
+ }
+ } else {
+ // roll up any popups if a top-level window is going away
+ if (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) {
+ RollUpPopups();
+ }
+
+ // now get rid of the window/sheet
+ if (mWindowType == WindowType::Sheet) {
+ if (mSheetNeedsShow) {
+ // This is an attempt to hide a sheet that never had a chance to
+ // be shown. There's nothing to do other than make sure that it
+ // won't show.
+ mSheetNeedsShow = false;
+ } else {
+ // get sheet's parent *before* hiding the sheet (which breaks the linkage)
+ NSWindow* sheetParent = mSheetWindowParent;
+
+ // hide the sheet
+#ifdef MOZ_THUNDERBIRD
+ [NSApp endSheet:mWindow];
+#else
+ [mSheetWindowParent endSheet:mWindow];
+#endif
+ [TopLevelWindowData deactivateInWindow:mWindow];
+
+ nsCOMPtr<nsIWidget> siblingSheetToShow;
+ bool parentIsSheet = false;
+
+ if (nativeParentWindow && piParentWidget &&
+ NS_SUCCEEDED(
+ piParentWidget->GetChildSheet(false, getter_AddRefs(siblingSheetToShow))) &&
+ siblingSheetToShow) {
+ // First, give sibling sheets an opportunity to show.
+ siblingSheetToShow->Show(true);
+ } else if (nativeParentWindow && piParentWidget &&
+ NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && parentIsSheet) {
+#ifdef MOZ_THUNDERBIRD
+ // Only set contextInfo if the parent of the parent sheet we're about
+ // to restore isn't itself a sheet.
+ NSWindow* contextInfo = sheetParent;
+#else
+ // Only set nonSheetGrandparent if the parent of the parent sheet we're about
+ // to restore isn't itself a sheet.
+ NSWindow* nonSheetGrandparent = sheetParent;
+#endif
+ nsIWidget* grandparentWidget = nil;
+ if (NS_SUCCEEDED(piParentWidget->GetRealParent(&grandparentWidget)) &&
+ grandparentWidget) {
+ nsCOMPtr<nsPIWidgetCocoa> piGrandparentWidget(do_QueryInterface(grandparentWidget));
+ bool grandparentIsSheet = false;
+ if (piGrandparentWidget &&
+ NS_SUCCEEDED(piGrandparentWidget->GetIsSheet(&grandparentIsSheet)) &&
+ grandparentIsSheet) {
+#ifdef MOZ_THUNDERBIRD
+ contextInfo = nil;
+#else
+ nonSheetGrandparent = nil;
+#endif
+ }
+ }
+ // If there are no sibling sheets, but the parent is a sheet, restore
+ // it. It wasn't sent any deactivate events when it was hidden, so
+ // don't call through Show, just let the OS put it back up.
+#ifdef MOZ_THUNDERBIRD
+ [NSApp beginSheet:nativeParentWindow
+ modalForWindow:sheetParent
+ modalDelegate:[nativeParentWindow delegate]
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:contextInfo];
+#else
+ [nativeParentWindow beginSheet:sheetParent
+ completionHandler:^(NSModalResponse returnCode) {
+ // Note: 'nonSheetGrandparent' (if it is set) is the window that is the
+ // parent of sheetParent. If it's set, 'nonSheetGrandparent' is always the
+ // top-level window, not another sheet itself. But 'nonSheetGrandparent'
+ // is nil if our parent window is also a sheet -- in that case we shouldn't
+ // send the top-level window any activate events (because it's our parent
+ // window that needs to get these events, not the top-level window).
+ [TopLevelWindowData deactivateInWindow:sheetParent];
+ [sheetParent orderOut:nil];
+ if (nonSheetGrandparent) {
+ [TopLevelWindowData activateInWindow:nonSheetGrandparent];
+ }
+ }];
+#endif
+ } else {
+ // Sheet, that was hard. No more siblings or parents, going back
+ // to a real window.
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [sheetParent makeKeyAndOrderFront:nil];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+ SendSetZLevelEvent();
+ }
+ } else {
+ // If the window is a popup window with a parent window we need to
+ // unhook it here before ordering it out. When you order out the child
+ // of a window it hides the parent window.
+ if (mWindowType == WindowType::Popup && nativeParentWindow)
+ [nativeParentWindow removeChildWindow:mWindow];
+
+ [mWindow orderOut:nil];
+
+ // If our popup window is a non-native context menu, tell the OS (and
+ // other programs) that a menu has closed.
+ if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) {
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.HIToolbox.endMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ }
+ }
+ }
+
+ [mWindow setBeingShown:NO];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Work around a problem where with multiple displays and multiple spaces
+// enabled, where the browser is assigned to a single display or space, popup
+// windows that are reshown after being hidden with [NSWindow orderOut] show on
+// the assigned space even when opened from another display. Apply the
+// workaround whenever more than one display is enabled.
+bool nsCocoaWindow::NeedsRecreateToReshow() {
+ // Limit the workaround to popup windows because only they need to override
+ // the "Assign To" setting. i.e., to display where the parent window is.
+ return (mWindowType == WindowType::Popup) && mWasShown && ([[NSScreen screens] count] > 1);
+}
+
+WindowRenderer* nsCocoaWindow::GetWindowRenderer() {
+ if (mPopupContentView) {
+ return mPopupContentView->GetWindowRenderer();
+ }
+ return nullptr;
+}
+
+TransparencyMode nsCocoaWindow::GetTransparencyMode() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (!mWindow || [mWindow isOpaque]) ? TransparencyMode::Opaque
+ : TransparencyMode::Transparent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(TransparencyMode::Opaque);
+}
+
+// This is called from nsMenuPopupFrame when making a popup transparent.
+void nsCocoaWindow::SetTransparencyMode(TransparencyMode aMode) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Only respect calls for popup windows.
+ if (!mWindow || mWindowType != WindowType::Popup) {
+ return;
+ }
+
+ BOOL isTransparent = aMode == TransparencyMode::Transparent;
+ BOOL currentTransparency = ![mWindow isOpaque];
+ if (isTransparent != currentTransparency) {
+ [mWindow setOpaque:!isTransparent];
+ [mWindow setBackgroundColor:(isTransparent ? [NSColor clearColor] : [NSColor whiteColor])];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::Enable(bool aState) {}
+
+bool nsCocoaWindow::IsEnabled() const { return true; }
+
+void nsCocoaWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || ![mWindow screen]) {
+ return;
+ }
+
+ nsIntRect screenBounds;
+
+ int32_t width, height;
+
+ NSRect frame = [mWindow frame];
+
+ // zero size rects confuse the screen manager
+ width = std::max<int32_t>(frame.size.width, 1);
+ height = std::max<int32_t>(frame.size.height, 1);
+
+ nsCOMPtr<nsIScreenManager> screenMgr = do_GetService("@mozilla.org/gfx/screenmanager;1");
+ if (screenMgr) {
+ nsCOMPtr<nsIScreen> screen;
+ screenMgr->ScreenForRect(aPoint.x, aPoint.y, width, height, getter_AddRefs(screen));
+
+ if (screen) {
+ screen->GetRectDisplayPix(&(screenBounds.x), &(screenBounds.y), &(screenBounds.width),
+ &(screenBounds.height));
+ }
+ }
+
+ if (aPoint.x < screenBounds.x) {
+ aPoint.x = screenBounds.x;
+ } else if (aPoint.x >= screenBounds.x + screenBounds.width - width) {
+ aPoint.x = screenBounds.x + screenBounds.width - width;
+ }
+
+ if (aPoint.y < screenBounds.y) {
+ aPoint.y = screenBounds.y;
+ } else if (aPoint.y >= screenBounds.y + screenBounds.height - height) {
+ aPoint.y = screenBounds.y + screenBounds.height - height;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Popups can be smaller than (32, 32)
+ NSRect rect = (mWindowType == WindowType::Popup) ? NSZeroRect : NSMakeRect(0.0, 0.0, 32, 32);
+ rect = [mWindow frameRectForChildViewRect:rect];
+
+ SizeConstraints c = aConstraints;
+
+ if (c.mScale.scale == MOZ_WIDGET_INVALID_SCALE) {
+ c.mScale.scale = BackingScaleFactor();
+ }
+
+ c.mMinSize.width = std::max(nsCocoaUtils::CocoaPointsToDevPixels(rect.size.width, c.mScale.scale),
+ c.mMinSize.width);
+ c.mMinSize.height = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(rect.size.height, c.mScale.scale), c.mMinSize.height);
+
+ NSSize minSize = {nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.width, c.mScale.scale),
+ nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.height, c.mScale.scale)};
+ [mWindow setMinSize:minSize];
+
+ c.mMaxSize.width = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.width, c.mScale.scale), c.mMaxSize.width);
+ c.mMaxSize.height = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.height, c.mScale.scale), c.mMaxSize.height);
+
+ NSSize maxSize = {c.mMaxSize.width == NS_MAXSIZE
+ ? FLT_MAX
+ : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.width, c.mScale.scale),
+ c.mMaxSize.height == NS_MAXSIZE
+ ? FLT_MAX
+ : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.height, c.mScale.scale)};
+ [mWindow setMaxSize:maxSize];
+
+ nsBaseWidget::SetSizeConstraints(c);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Move(double aX, double aY) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ // The point we have is in Gecko coordinates (origin top-left). Convert
+ // it to Cocoa ones (origin bottom-left).
+ NSPoint coord = {static_cast<float>(aX),
+ static_cast<float>(nsCocoaUtils::FlippedScreenY(NSToIntRound(aY)))};
+
+ NSRect frame = [mWindow frame];
+ if (frame.origin.x != coord.x || frame.origin.y + frame.size.height != coord.y) {
+ [mWindow setFrameTopLeftPoint:coord];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSizeMode(nsSizeMode aMode) {
+ if (aMode == nsSizeMode_Normal) {
+ QueueTransition(TransitionType::Windowed);
+ } else if (aMode == nsSizeMode_Minimized) {
+ QueueTransition(TransitionType::Miniaturize);
+ } else if (aMode == nsSizeMode_Maximized) {
+ QueueTransition(TransitionType::Zoom);
+ } else if (aMode == nsSizeMode_Fullscreen) {
+ MakeFullScreen(true);
+ }
+}
+
+// The (work)space switching implementation below was inspired by Phoenix:
+// https://github.com/kasper/phoenix/tree/d6c877f62b30a060dff119d8416b0934f76af534
+// License: MIT.
+
+// Runtime `CGSGetActiveSpace` library function feature detection.
+typedef CGSSpaceID (*CGSGetActiveSpaceFunc)(CGSConnection cid);
+static CGSGetActiveSpaceFunc GetCGSGetActiveSpaceFunc() {
+ static CGSGetActiveSpaceFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSGetActiveSpaceFunc)dlsym(RTLD_DEFAULT, "CGSGetActiveSpace");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSCopyManagedDisplaySpaces` library function feature detection.
+typedef CFArrayRef (*CGSCopyManagedDisplaySpacesFunc)(CGSConnection cid);
+static CGSCopyManagedDisplaySpacesFunc GetCGSCopyManagedDisplaySpacesFunc() {
+ static CGSCopyManagedDisplaySpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSCopyManagedDisplaySpacesFunc)dlsym(RTLD_DEFAULT, "CGSCopyManagedDisplaySpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSCopySpacesForWindows` library function feature detection.
+typedef CFArrayRef (*CGSCopySpacesForWindowsFunc)(CGSConnection cid, CGSSpaceMask mask,
+ CFArrayRef windowIDs);
+static CGSCopySpacesForWindowsFunc GetCGSCopySpacesForWindowsFunc() {
+ static CGSCopySpacesForWindowsFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSCopySpacesForWindowsFunc)dlsym(RTLD_DEFAULT, "CGSCopySpacesForWindows");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSAddWindowsToSpaces` library function feature detection.
+typedef void (*CGSAddWindowsToSpacesFunc)(CGSConnection cid, CFArrayRef windowIDs,
+ CFArrayRef spaceIDs);
+static CGSAddWindowsToSpacesFunc GetCGSAddWindowsToSpacesFunc() {
+ static CGSAddWindowsToSpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSAddWindowsToSpacesFunc)dlsym(RTLD_DEFAULT, "CGSAddWindowsToSpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSRemoveWindowsFromSpaces` library function feature detection.
+typedef void (*CGSRemoveWindowsFromSpacesFunc)(CGSConnection cid, CFArrayRef windowIDs,
+ CFArrayRef spaceIDs);
+static CGSRemoveWindowsFromSpacesFunc GetCGSRemoveWindowsFromSpacesFunc() {
+ static CGSRemoveWindowsFromSpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSRemoveWindowsFromSpacesFunc)dlsym(RTLD_DEFAULT, "CGSRemoveWindowsFromSpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+
+void nsCocoaWindow::GetWorkspaceID(nsAString& workspaceID) {
+ workspaceID.Truncate();
+ int32_t sid = GetWorkspaceID();
+ if (sid != 0) {
+ workspaceID.AppendInt(sid);
+ }
+}
+
+int32_t nsCocoaWindow::GetWorkspaceID() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Mac OSX space IDs start at '1' (default space), so '0' means 'unknown',
+ // effectively.
+ CGSSpaceID sid = 0;
+
+ CGSCopySpacesForWindowsFunc CopySpacesForWindows = GetCGSCopySpacesForWindowsFunc();
+ if (!CopySpacesForWindows) {
+ return sid;
+ }
+
+ CGSConnection cid = _CGSDefaultConnection();
+ // Fetch all spaces that this window belongs to (in order).
+ NSArray<NSNumber*>* spaceIDs = CFBridgingRelease(CopySpacesForWindows(
+ cid, kCGSAllSpacesMask, (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ]));
+ if ([spaceIDs count]) {
+ // When spaces are found, return the first one.
+ // We don't support a single window painted across multiple places for now.
+ sid = [spaceIDs[0] integerValue];
+ } else {
+ // Fall back to the workspace that's currently active, which is '1' in the
+ // common case.
+ CGSGetActiveSpaceFunc GetActiveSpace = GetCGSGetActiveSpaceFunc();
+ if (GetActiveSpace) {
+ sid = GetActiveSpace(cid);
+ }
+ }
+
+ return sid;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::MoveToWorkspace(const nsAString& workspaceIDStr) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([NSScreen screensHaveSeparateSpaces] && [[NSScreen screens] count] > 1) {
+ // We don't support moving to a workspace when the user has this option
+ // enabled in Mission Control.
+ return;
+ }
+
+ nsresult rv = NS_OK;
+ int32_t workspaceID = workspaceIDStr.ToInteger(&rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ CGSConnection cid = _CGSDefaultConnection();
+ int32_t currentSpace = GetWorkspaceID();
+ // If an empty workspace ID is passed in (not valid on OSX), or when the
+ // window is already on this workspace, we don't need to do anything.
+ if (!workspaceID || workspaceID == currentSpace) {
+ return;
+ }
+
+ CGSCopyManagedDisplaySpacesFunc CopyManagedDisplaySpaces = GetCGSCopyManagedDisplaySpacesFunc();
+ CGSAddWindowsToSpacesFunc AddWindowsToSpaces = GetCGSAddWindowsToSpacesFunc();
+ CGSRemoveWindowsFromSpacesFunc RemoveWindowsFromSpaces = GetCGSRemoveWindowsFromSpacesFunc();
+ if (!CopyManagedDisplaySpaces || !AddWindowsToSpaces || !RemoveWindowsFromSpaces) {
+ return;
+ }
+
+ // Fetch an ordered list of all known spaces.
+ NSArray* displaySpacesInfo = CFBridgingRelease(CopyManagedDisplaySpaces(cid));
+ // When we found the space we're looking for, we can bail out of the loop
+ // early, which this local variable is used for.
+ BOOL found = false;
+ for (NSDictionary<NSString*, id>* spacesInfo in displaySpacesInfo) {
+ NSArray<NSNumber*>* sids = [spacesInfo[CGSSpacesKey] valueForKey:CGSSpaceIDKey];
+ for (NSNumber* sid in sids) {
+ // If we found our space in the list, we're good to go and can jump out of
+ // this loop.
+ if ((int)[sid integerValue] == workspaceID) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ break;
+ }
+ }
+
+ // We were unable to find the space to correspond with the workspaceID as
+ // requested, so let's bail out.
+ if (!found) {
+ return;
+ }
+
+ // First we add the window to the appropriate space.
+ AddWindowsToSpaces(cid, (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
+ (__bridge CFArrayRef) @[ @(workspaceID) ]);
+ // Then we remove the window from the active space.
+ RemoveWindowsFromSpaces(cid, (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
+ (__bridge CFArrayRef) @[ @(currentSpace) ]);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SuppressAnimation(bool aSuppress) {
+ if ([mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
+ if (aSuppress) {
+ [mWindow setIsAnimationSuppressed:YES];
+ [mWindow setAnimationBehavior:NSWindowAnimationBehaviorNone];
+ } else {
+ [mWindow setIsAnimationSuppressed:NO];
+ [mWindow setAnimationBehavior:mWindowAnimationBehavior];
+ }
+ }
+}
+
+// This has to preserve the window's frame bounds.
+// This method requires (as does the Windows impl.) that you call Resize shortly
+// after calling HideWindowChrome. See bug 498835 for fixing this.
+void nsCocoaWindow::HideWindowChrome(bool aShouldHide) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || !mWindowMadeHere ||
+ (mWindowType != WindowType::TopLevel && mWindowType != WindowType::Dialog))
+ return;
+
+ BOOL isVisible = [mWindow isVisible];
+
+ // Remove child windows.
+ NSArray* childWindows = [mWindow childWindows];
+ NSEnumerator* enumerator = [childWindows objectEnumerator];
+ NSWindow* child = nil;
+ while ((child = [enumerator nextObject])) {
+ [mWindow removeChildWindow:child];
+ }
+
+ // Remove the views in the old window's content view.
+ // The NSArray is autoreleased and retains its NSViews.
+ NSArray<NSView*>* contentViewContents = [mWindow contentViewContents];
+ for (NSView* view in contentViewContents) {
+ [view removeFromSuperviewWithoutNeedingDisplay];
+ }
+
+ // Save state (like window title).
+ NSMutableDictionary* state = [mWindow exportState];
+
+ // Recreate the window with the right border style.
+ NSRect frameRect = [mWindow frame];
+ DestroyNativeWindow();
+ nsresult rv = CreateNativeWindow(frameRect, aShouldHide ? BorderStyle::None : mBorderStyle, true,
+ mWindow.restorable);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Re-import state.
+ [mWindow importState:state];
+
+ // Add the old content view subviews to the new window's content view.
+ for (NSView* view in contentViewContents) {
+ [[mWindow contentView] addSubview:view];
+ }
+
+ // Reparent child windows.
+ enumerator = [childWindows objectEnumerator];
+ while ((child = [enumerator nextObject])) {
+ [mWindow addChildWindow:child ordered:NSWindowAbove];
+ }
+
+ // Show the new window.
+ if (isVisible) {
+ bool wasAnimationSuppressed = mIsAnimationSuppressed;
+ mIsAnimationSuppressed = true;
+ Show(true);
+ mIsAnimationSuppressed = wasAnimationSuppressed;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+class FullscreenTransitionData : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit FullscreenTransitionData(NSWindow* aWindow) : mTransitionWindow(aWindow) {}
+
+ NSWindow* mTransitionWindow;
+
+ private:
+ virtual ~FullscreenTransitionData() { [mTransitionWindow close]; }
+};
+
+NS_IMPL_ISUPPORTS0(FullscreenTransitionData)
+
+@interface FullscreenTransitionDelegate : NSObject <NSAnimationDelegate> {
+ @public
+ nsCocoaWindow* mWindow;
+ nsIRunnable* mCallback;
+}
+@end
+
+@implementation FullscreenTransitionDelegate
+- (void)cleanupAndDispatch:(NSAnimation*)animation {
+ [animation setDelegate:nil];
+ [self autorelease];
+ // The caller should have added ref for us.
+ NS_DispatchToMainThread(already_AddRefed<nsIRunnable>(mCallback));
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ MOZ_ASSERT(animation == mWindow->FullscreenTransitionAnimation(),
+ "Should be handling the only animation on the window");
+ mWindow->ReleaseFullscreenTransitionAnimation();
+ [self cleanupAndDispatch:animation];
+}
+
+- (void)animationDidStop:(NSAnimation*)animation {
+ [self cleanupAndDispatch:animation];
+}
+@end
+
+static bool AlwaysUsesNativeFullScreen() {
+ return Preferences::GetBool("full-screen-api.macos-native-full-screen", false);
+}
+
+/* virtual */ bool nsCocoaWindow::PrepareForFullscreenTransition(nsISupports** aData) {
+ if (AlwaysUsesNativeFullScreen()) {
+ return false;
+ }
+
+ // Our fullscreen transition creates a new window occluding this window.
+ // That triggers an occlusion event which can cause DOM fullscreen requests
+ // to fail due to the context not being focused at the time the focus check
+ // is performed in the child process. Until the transition is cleaned up in
+ // CleanupFullscreenTransition(), ignore occlusion events for this window.
+ // If this method is changed to return false, the transition will not be
+ // performed and mIgnoreOcclusionCount should not be incremented.
+ MOZ_ASSERT(mIgnoreOcclusionCount >= 0);
+ mIgnoreOcclusionCount++;
+
+ nsCOMPtr<nsIScreen> widgetScreen = GetWidgetScreen();
+ NSScreen* cocoaScreen = ScreenHelperCocoa::CocoaScreenForScreen(widgetScreen);
+
+ NSWindow* win = [[NSWindow alloc] initWithContentRect:[cocoaScreen frame]
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:YES];
+ [win setBackgroundColor:[NSColor blackColor]];
+ [win setAlphaValue:0];
+ [win setIgnoresMouseEvents:YES];
+ [win setLevel:NSScreenSaverWindowLevel];
+ [win makeKeyAndOrderFront:nil];
+
+ auto data = new FullscreenTransitionData(win);
+ *aData = data;
+ NS_ADDREF(data);
+ return true;
+}
+
+/* virtual */ void nsCocoaWindow::CleanupFullscreenTransition() {
+ MOZ_ASSERT(mIgnoreOcclusionCount > 0);
+ mIgnoreOcclusionCount--;
+}
+
+/* virtual */ void nsCocoaWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) {
+ auto data = static_cast<FullscreenTransitionData*>(aData);
+ FullscreenTransitionDelegate* delegate = [[FullscreenTransitionDelegate alloc] init];
+ delegate->mWindow = this;
+ // Storing already_AddRefed directly could cause static checking fail.
+ delegate->mCallback = nsCOMPtr<nsIRunnable>(aCallback).forget().take();
+
+ if (mFullscreenTransitionAnimation) {
+ [mFullscreenTransitionAnimation stopAnimation];
+ ReleaseFullscreenTransitionAnimation();
+ }
+
+ NSDictionary* dict = @{
+ NSViewAnimationTargetKey : data->mTransitionWindow,
+ NSViewAnimationEffectKey : aStage == eBeforeFullscreenToggle ? NSViewAnimationFadeInEffect
+ : NSViewAnimationFadeOutEffect
+ };
+ mFullscreenTransitionAnimation = [[NSViewAnimation alloc] initWithViewAnimations:@[ dict ]];
+ [mFullscreenTransitionAnimation setDelegate:delegate];
+ [mFullscreenTransitionAnimation setDuration:aDuration / 1000.0];
+ [mFullscreenTransitionAnimation startAnimation];
+}
+
+void nsCocoaWindow::CocoaWindowWillEnterFullscreen(bool aFullscreen) {
+ MOZ_ASSERT(mUpdateFullscreenOnResize.isNothing());
+
+ mHasStartedNativeFullscreen = true;
+
+ // Ensure that we update our fullscreen state as early as possible, when the resize
+ // happens.
+ mUpdateFullscreenOnResize =
+ Some(aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed);
+}
+
+void nsCocoaWindow::CocoaWindowDidEnterFullscreen(bool aFullscreen) {
+ mHasStartedNativeFullscreen = false;
+ DispatchOcclusionEvent();
+ HandleUpdateFullscreenOnResize();
+ FinishCurrentTransitionIfMatching(aFullscreen ? TransitionType::Fullscreen
+ : TransitionType::Windowed);
+}
+
+void nsCocoaWindow::CocoaWindowDidFailFullscreen(bool aAttemptedFullscreen) {
+ mHasStartedNativeFullscreen = false;
+ DispatchOcclusionEvent();
+
+ // If we already updated our fullscreen state due to a resize, we need to update it again.
+ if (mUpdateFullscreenOnResize.isNothing()) {
+ UpdateFullscreenState(!aAttemptedFullscreen, true);
+ ReportSizeEvent();
+ }
+
+ TransitionType transition =
+ aAttemptedFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed;
+ FinishCurrentTransitionIfMatching(transition);
+}
+
+void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) {
+ bool wasInFullscreen = mInFullScreenMode;
+ mInFullScreenMode = aFullScreen;
+ if (aNativeMode || mInNativeFullScreenMode) {
+ mInNativeFullScreenMode = aFullScreen;
+ }
+
+ if (aFullScreen == wasInFullscreen) {
+ return;
+ }
+
+ DispatchSizeModeEvent();
+
+ // Notify the mainChildView with our new fullscreen state.
+ nsChildView* mainChildView = static_cast<nsChildView*>([[mWindow mainChildView] widget]);
+ if (mainChildView) {
+ mainChildView->UpdateFullscreen(aFullScreen);
+ }
+}
+
+nsresult nsCocoaWindow::MakeFullScreen(bool aFullScreen) {
+ return DoMakeFullScreen(aFullScreen, AlwaysUsesNativeFullScreen());
+}
+
+nsresult nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen) {
+ return DoMakeFullScreen(aFullScreen, true);
+}
+
+nsresult nsCocoaWindow::DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition) {
+ if (!mWindow) {
+ return NS_OK;
+ }
+
+ // Figure out what type of transition is being requested.
+ TransitionType transition = TransitionType::Windowed;
+ if (aFullScreen) {
+ // Decide whether to use fullscreen or emulated fullscreen.
+ transition = (aUseSystemTransition &&
+ (mWindow.collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary))
+ ? TransitionType::Fullscreen
+ : TransitionType::EmulatedFullscreen;
+ }
+
+ QueueTransition(transition);
+ return NS_OK;
+}
+
+void nsCocoaWindow::QueueTransition(const TransitionType& aTransition) {
+ mTransitionsPending.push(aTransition);
+ ProcessTransitions();
+}
+
+void nsCocoaWindow::ProcessTransitions() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ if (mInProcessTransitions) {
+ return;
+ }
+
+ mInProcessTransitions = true;
+
+ // Start a loop that will continue as long as we have transitions to process
+ // and we aren't waiting on an asynchronous transition to complete. Any
+ // transition that starts something async will `continue` this loop to exit.
+ while (!mTransitionsPending.empty() && !IsInTransition()) {
+ TransitionType nextTransition = mTransitionsPending.front();
+
+ // We have to check for some incompatible transition states, and if we find one,
+ // instead perform an alternative transition and leave the queue untouched. If
+ // we add one of these transitions, we set mIsTransitionCurrentAdded because we
+ // don't want to confuse listeners who are expecting to receive exactly one
+ // event when the requested transition has completed.
+ switch (nextTransition) {
+ case TransitionType::Fullscreen:
+ case TransitionType::EmulatedFullscreen:
+ case TransitionType::Windowed:
+ case TransitionType::Zoom:
+ // These can't handle miniaturized windows, so deminiaturize first.
+ if (mWindow.miniaturized) {
+ mTransitionCurrent = Some(TransitionType::Deminiaturize);
+ mIsTransitionCurrentAdded = true;
+ }
+ break;
+ case TransitionType::Miniaturize:
+ // This can't handle fullscreen, so go to windowed first.
+ if (mInFullScreenMode) {
+ mTransitionCurrent = Some(TransitionType::Windowed);
+ mIsTransitionCurrentAdded = true;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // If mTransitionCurrent is still empty, then we use the nextTransition and pop
+ // the queue.
+ if (mTransitionCurrent.isNothing()) {
+ mTransitionCurrent = Some(nextTransition);
+ mTransitionsPending.pop();
+ }
+
+ switch (*mTransitionCurrent) {
+ case TransitionType::Fullscreen: {
+ if (!mInFullScreenMode) {
+ // This triggers an async animation, so continue.
+ [mWindow toggleFullScreen:nil];
+ continue;
+ }
+ break;
+ }
+
+ case TransitionType::EmulatedFullscreen: {
+ if (!mInFullScreenMode) {
+ NSDisableScreenUpdates();
+ mSuppressSizeModeEvents = true;
+ // The order here matters. When we exit full screen mode, we need to show the
+ // Dock first, otherwise the newly-created window won't have its minimize
+ // button enabled. See bug 526282.
+ nsCocoaUtils::HideOSChromeOnScreen(true);
+ nsBaseWidget::InfallibleMakeFullScreen(true);
+ mSuppressSizeModeEvents = false;
+ NSEnableScreenUpdates();
+ UpdateFullscreenState(true, false);
+ }
+ break;
+ }
+
+ case TransitionType::Windowed: {
+ if (mInFullScreenMode) {
+ if (mInNativeFullScreenMode) {
+ // This triggers an async animation, so continue.
+ [mWindow toggleFullScreen:nil];
+ continue;
+ } else {
+ NSDisableScreenUpdates();
+ mSuppressSizeModeEvents = true;
+ // The order here matters. When we exit full screen mode, we need to show the
+ // Dock first, otherwise the newly-created window won't have its minimize
+ // button enabled. See bug 526282.
+ nsCocoaUtils::HideOSChromeOnScreen(false);
+ nsBaseWidget::InfallibleMakeFullScreen(false);
+ mSuppressSizeModeEvents = false;
+ NSEnableScreenUpdates();
+ UpdateFullscreenState(false, false);
+ }
+ } else if (mWindow.zoomed) {
+ [mWindow zoom:nil];
+
+ // Check if we're still zoomed. If we are, we need to do *something* to make the
+ // window smaller than the zoom size so Cocoa will treat us as being out of the
+ // zoomed state. Otherwise, we could stay zoomed and never be able to be "normal"
+ // from calls to SetSizeMode.
+ if (mWindow.zoomed) {
+ NSRect maximumFrame = mWindow.frame;
+ const CGFloat INSET_OUT_OF_ZOOM = 20.0f;
+ [mWindow setFrame:NSInsetRect(maximumFrame, INSET_OUT_OF_ZOOM, INSET_OUT_OF_ZOOM)
+ display:YES];
+ MOZ_ASSERT(!mWindow.zoomed,
+ "We should be able to unzoom by shrinking the frame a bit.");
+ }
+ }
+ break;
+ }
+
+ case TransitionType::Miniaturize:
+ if (!mWindow.miniaturized) {
+ // This triggers an async animation, so continue.
+ [mWindow miniaturize:nil];
+ continue;
+ }
+ break;
+
+ case TransitionType::Deminiaturize:
+ if (mWindow.miniaturized) {
+ // This triggers an async animation, so continue.
+ [mWindow deminiaturize:nil];
+ continue;
+ }
+ break;
+
+ case TransitionType::Zoom:
+ if (!mWindow.zoomed) {
+ [mWindow zoom:nil];
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ mTransitionCurrent.reset();
+ mIsTransitionCurrentAdded = false;
+ }
+
+ mInProcessTransitions = false;
+
+ // When we finish processing transitions, dispatch a size mode event to cover the
+ // cases where an inserted transition suppressed one, and the original transition
+ // never sent one because it detected it was at the desired state when it ran. If
+ // we've already sent a size mode event, then this will be a no-op.
+ if (!IsInTransition()) {
+ DispatchSizeModeEvent();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::FinishCurrentTransition() {
+ mTransitionCurrent.reset();
+ mIsTransitionCurrentAdded = false;
+ ProcessTransitions();
+}
+
+void nsCocoaWindow::FinishCurrentTransitionIfMatching(const TransitionType& aTransition) {
+ // We've just finished some transition activity, and we're not sure whether it was
+ // triggered programmatically, or by the user. If it matches our current transition,
+ // then assume it was triggered programmatically and we can clean up that transition
+ // and start processing transitions again.
+
+ // Whether programmatic or user-initiated, we send out a size mode event.
+ DispatchSizeModeEvent();
+
+ if (mTransitionCurrent.isSome() && (*mTransitionCurrent == aTransition)) {
+ // This matches our current transition. Since this function is called from
+ // nsWindowDelegate transition callbacks, we want to make sure those callbacks are
+ // all the way done before we start processing more transitions. To accomplish this,
+ // we dispatch our cleanup to happen on the next event loop. Doing this will ensure
+ // that any async native transition methods we call (like toggleFullscreen) will
+ // succeed.
+ NS_DispatchToCurrentThread(NewRunnableMethod("FinishCurrentTransition", this,
+ &nsCocoaWindow::FinishCurrentTransition));
+ }
+}
+
+bool nsCocoaWindow::HandleUpdateFullscreenOnResize() {
+ if (mUpdateFullscreenOnResize.isNothing()) {
+ return false;
+ }
+
+ bool toFullscreen = (*mUpdateFullscreenOnResize == TransitionType::Fullscreen);
+ mUpdateFullscreenOnResize.reset();
+ UpdateFullscreenState(toFullscreen, true);
+
+ return true;
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::DoResize(double aX, double aY, double aWidth, double aHeight, bool aRepaint,
+ bool aConstrainToCurrentScreen) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || mInResize) {
+ return;
+ }
+
+ // We are able to resize a window outside of any aspect ratio contraints
+ // applied to it, but in order to "update" the aspect ratio contraint to the
+ // new window dimensions, we must re-lock the aspect ratio.
+ auto relockAspectRatio = MakeScopeExit([&]() {
+ if (mAspectRatioLocked) {
+ LockAspectRatio(true);
+ }
+ });
+
+ AutoRestore<bool> reentrantResizeGuard(mInResize);
+ mInResize = true;
+
+ CGFloat scale = mSizeConstraints.mScale.scale;
+ if (scale == MOZ_WIDGET_INVALID_SCALE) {
+ scale = BackingScaleFactor();
+ }
+
+ // mSizeConstraints is in device pixels.
+ int32_t width = NSToIntRound(aWidth * scale);
+ int32_t height = NSToIntRound(aHeight * scale);
+
+ width =
+ std::max(mSizeConstraints.mMinSize.width, std::min(mSizeConstraints.mMaxSize.width, width));
+ height = std::max(mSizeConstraints.mMinSize.height,
+ std::min(mSizeConstraints.mMaxSize.height, height));
+
+ DesktopIntRect newBounds(NSToIntRound(aX), NSToIntRound(aY), NSToIntRound(width / scale),
+ NSToIntRound(height / scale));
+
+ // constrain to the screen that contains the largest area of the new rect
+ FitRectToVisibleAreaForScreen(newBounds, aConstrainToCurrentScreen ? [mWindow screen] : nullptr);
+
+ // convert requested bounds into Cocoa coordinate system
+ NSRect newFrame = nsCocoaUtils::GeckoRectToCocoaRect(newBounds);
+
+ NSRect frame = [mWindow frame];
+ BOOL isMoving = newFrame.origin.x != frame.origin.x || newFrame.origin.y != frame.origin.y;
+ BOOL isResizing =
+ newFrame.size.width != frame.size.width || newFrame.size.height != frame.size.height;
+
+ if (!isMoving && !isResizing) {
+ return;
+ }
+
+ // We ignore aRepaint -- we have to call display:YES, otherwise the
+ // title bar doesn't immediately get repainted and is displayed in
+ // the wrong place, leading to a visual jump.
+ [mWindow setFrame:newFrame display:YES];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Resize(double aX, double aY, double aWidth, double aHeight, bool aRepaint) {
+ DoResize(aX, aY, aWidth, aHeight, aRepaint, false);
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ double invScale = 1.0 / BackingScaleFactor();
+ DoResize(mBounds.x * invScale, mBounds.y * invScale, aWidth, aHeight, aRepaint, true);
+}
+
+// Return the area that the Gecko ChildView in our window should cover, as an
+// NSRect in screen coordinates (with 0,0 being the bottom left corner of the
+// primary screen).
+NSRect nsCocoaWindow::GetClientCocoaRect() {
+ if (!mWindow) {
+ return NSZeroRect;
+ }
+
+ return [mWindow childViewRectForFrameRect:[mWindow frame]];
+}
+
+LayoutDeviceIntRect nsCocoaWindow::GetClientBounds() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = BackingScaleFactor();
+ return nsCocoaUtils::CocoaRectToGeckoRectDevPix(GetClientCocoaRect(), scaleFactor);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0));
+}
+
+void nsCocoaWindow::UpdateBounds() {
+ NSRect frame = NSZeroRect;
+ if (mWindow) {
+ frame = [mWindow frame];
+ }
+ mBounds = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor());
+
+ if (mPopupContentView) {
+ mPopupContentView->UpdateBoundsFromView();
+ }
+}
+
+LayoutDeviceIntRect nsCocoaWindow::GetScreenBounds() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef DEBUG
+ LayoutDeviceIntRect r =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix([mWindow frame], BackingScaleFactor());
+ NS_ASSERTION(mWindow && mBounds == r, "mBounds out of sync!");
+#endif
+
+ return mBounds;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0));
+}
+
+double nsCocoaWindow::GetDefaultScaleInternal() { return BackingScaleFactor(); }
+
+static CGFloat GetBackingScaleFactor(NSWindow* aWindow) {
+ NSRect frame = [aWindow frame];
+ if (frame.size.width > 0 && frame.size.height > 0) {
+ return nsCocoaUtils::GetBackingScaleFactor(aWindow);
+ }
+
+ // For windows with zero width or height, the backingScaleFactor method
+ // is broken - it will always return 2 on a retina macbook, even when
+ // the window position implies it's on a non-hidpi external display
+ // (to the extent that a zero-area window can be said to be "on" a
+ // display at all!)
+ // And to make matters worse, Cocoa even fires a
+ // windowDidChangeBackingProperties notification with the
+ // NSBackingPropertyOldScaleFactorKey key when a window on an
+ // external display is resized to/from zero height, even though it hasn't
+ // really changed screens.
+
+ // This causes us to handle popup window sizing incorrectly when the
+ // popup is resized to zero height (bug 820327) - nsXULPopupManager
+ // becomes (incorrectly) convinced the popup has been explicitly forced
+ // to a non-default size and needs to have size attributes attached.
+
+ // Workaround: instead of asking the window, we'll find the screen it is on
+ // and ask that for *its* backing scale factor.
+
+ // (See bug 853252 and additional comments in windowDidChangeScreen: below
+ // for further complications this causes.)
+
+ // First, expand the rect so that it actually has a measurable area,
+ // for FindTargetScreenForRect to use.
+ if (frame.size.width == 0) {
+ frame.size.width = 1;
+ }
+ if (frame.size.height == 0) {
+ frame.size.height = 1;
+ }
+
+ // Then identify the screen it belongs to, and return its scale factor.
+ NSScreen* screen = FindTargetScreenForRect(nsCocoaUtils::CocoaRectToGeckoRect(frame));
+ return nsCocoaUtils::GetBackingScaleFactor(screen);
+}
+
+CGFloat nsCocoaWindow::BackingScaleFactor() {
+ if (mBackingScaleFactor > 0.0) {
+ return mBackingScaleFactor;
+ }
+ if (!mWindow) {
+ return 1.0;
+ }
+ mBackingScaleFactor = GetBackingScaleFactor(mWindow);
+ return mBackingScaleFactor;
+}
+
+void nsCocoaWindow::BackingScaleFactorChanged() {
+ CGFloat newScale = GetBackingScaleFactor(mWindow);
+
+ // ignore notification if it hasn't really changed (or maybe we have
+ // disabled HiDPI mode via prefs)
+ if (mBackingScaleFactor == newScale) {
+ return;
+ }
+
+ mBackingScaleFactor = newScale;
+
+ if (!mWidgetListener || mWidgetListener->GetAppWindow()) {
+ return;
+ }
+
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ mWidgetListener->UIResolutionChanged();
+}
+
+int32_t nsCocoaWindow::RoundsWidgetCoordinatesTo() {
+ if (BackingScaleFactor() == 2.0) {
+ return 2;
+ }
+ return 1;
+}
+
+void nsCocoaWindow::SetCursor(const Cursor& aCursor) {
+ if (mPopupContentView) {
+ mPopupContentView->SetCursor(aCursor);
+ }
+}
+
+nsresult nsCocoaWindow::SetTitle(const nsAString& aTitle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mWindow) {
+ return NS_OK;
+ }
+
+ const nsString& strTitle = PromiseFlatString(aTitle);
+ const unichar* uniTitle = reinterpret_cast<const unichar*>(strTitle.get());
+ NSString* title = [NSString stringWithCharacters:uniTitle length:strTitle.Length()];
+ if ([mWindow drawsContentsIntoWindowFrame] && ![mWindow wantsTitleDrawn]) {
+ // Don't cause invalidations when the title isn't displayed.
+ [mWindow disableSetNeedsDisplay];
+ [mWindow setTitle:title];
+ [mWindow enableSetNeedsDisplay];
+ } else {
+ [mWindow setTitle:title];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
+ if (mPopupContentView) {
+ mPopupContentView->Invalidate(aRect);
+ }
+}
+
+// Pass notification of some drag event to Gecko
+//
+// The drag manager has let us know that something related to a drag has
+// occurred in this window. It could be any number of things, ranging from
+// a drop, to a drag enter/leave, or a drag over event. The actual event
+// is passed in |aMessage| and is passed along to our event hanlder so Gecko
+// knows about it.
+bool nsCocoaWindow::DragEvent(unsigned int aMessage, mozilla::gfx::Point aMouseGlobal,
+ UInt16 aKeyModifiers) {
+ return false;
+}
+
+NS_IMETHODIMP nsCocoaWindow::SendSetZLevelEvent() {
+ nsWindowZ placement = nsWindowZTop;
+ nsCOMPtr<nsIWidget> actualBelow;
+ if (mWidgetListener)
+ mWidgetListener->ZLevelChanged(true, &placement, nullptr, getter_AddRefs(actualBelow));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetChildSheet(bool aShown, nsIWidget** _retval) {
+ nsIWidget* child = GetFirstChild();
+
+ while (child) {
+ if (child->GetWindowType() == WindowType::Sheet) {
+ // if it's a sheet, it must be an nsCocoaWindow
+ nsCocoaWindow* cocoaWindow = static_cast<nsCocoaWindow*>(child);
+ if (cocoaWindow->mWindow && ((aShown && [cocoaWindow->mWindow isVisible]) ||
+ (!aShown && cocoaWindow->mSheetNeedsShow))) {
+ nsCOMPtr<nsIWidget> widget = cocoaWindow;
+ widget.forget(_retval);
+ return NS_OK;
+ }
+ }
+ child = child->GetNextSibling();
+ }
+
+ *_retval = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetRealParent(nsIWidget** parent) {
+ *parent = mParent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetIsSheet(bool* isSheet) {
+ mWindowType == WindowType::Sheet ? * isSheet = true : * isSheet = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetSheetWindowParent(NSWindow** sheetWindowParent) {
+ *sheetWindowParent = mSheetWindowParent;
+ return NS_OK;
+}
+
+// Invokes callback and ProcessEvent methods on Event Listener object
+nsresult nsCocoaWindow::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(event->mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not used within this function
+
+ if (mWidgetListener) aStatus = mWidgetListener->HandleEvent(event, mUseAttachedEvents);
+
+ return NS_OK;
+}
+
+// aFullScreen should be the window's mInFullScreenMode. We don't have access to that
+// from here, so we need to pass it in. mInFullScreenMode should be the canonical
+// indicator that a window is currently full screen and it makes sense to keep
+// all sizemode logic here.
+static nsSizeMode GetWindowSizeMode(NSWindow* aWindow, bool aFullScreen) {
+ if (aFullScreen) return nsSizeMode_Fullscreen;
+ if ([aWindow isMiniaturized]) return nsSizeMode_Minimized;
+ if (([aWindow styleMask] & NSWindowStyleMaskResizable) && [aWindow isZoomed])
+ return nsSizeMode_Maximized;
+ return nsSizeMode_Normal;
+}
+
+void nsCocoaWindow::ReportMoveEvent() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Prevent recursion, which can become infinite (see bug 708278). This
+ // can happen when the call to [NSWindow setFrameTopLeftPoint:] in
+ // nsCocoaWindow::Move() triggers an immediate NSWindowDidMove notification
+ // (and a call to [WindowDelegate windowDidMove:]).
+ if (mInReportMoveEvent) {
+ return;
+ }
+ mInReportMoveEvent = true;
+
+ UpdateBounds();
+
+ // The zoomed state can change when we're moving, in which case we need to
+ // update our internal mSizeMode. This can happen either if we're maximized
+ // and then moved, or if we're not maximized and moved back to zoomed state.
+ if (mWindow && ((mSizeMode == nsSizeMode_Maximized) ^ [mWindow isZoomed])) {
+ DispatchSizeModeEvent();
+ }
+
+ // Dispatch the move event to Gecko
+ NotifyWindowMoved(mBounds.x, mBounds.y);
+
+ mInReportMoveEvent = false;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::DispatchSizeModeEvent() {
+ if (!mWindow) {
+ return;
+ }
+
+ if (mSuppressSizeModeEvents || mIsTransitionCurrentAdded) {
+ return;
+ }
+
+ nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode);
+ if (mSizeMode == newMode) {
+ return;
+ }
+
+ mSizeMode = newMode;
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(newMode);
+ }
+
+ if (StaticPrefs::widget_pause_compositor_when_minimized()) {
+ if (newMode == nsSizeMode_Minimized) {
+ PauseCompositor();
+ } else {
+ ResumeCompositor();
+ }
+ }
+}
+
+void nsCocoaWindow::DispatchOcclusionEvent() {
+ if (!mWindow) {
+ return;
+ }
+
+ // Our new occlusion state is true if the window is not visible.
+ bool newOcclusionState =
+ !(mHasStartedNativeFullscreen || ([mWindow occlusionState] & NSWindowOcclusionStateVisible));
+
+ // Don't dispatch if the new occlustion state is the same as the current state.
+ if (mIsFullyOccluded == newOcclusionState) {
+ return;
+ }
+
+ MOZ_ASSERT(mIgnoreOcclusionCount >= 0);
+ if (newOcclusionState && mIgnoreOcclusionCount > 0) {
+ return;
+ }
+
+ mIsFullyOccluded = newOcclusionState;
+ if (mWidgetListener) {
+ mWidgetListener->OcclusionStateChanged(mIsFullyOccluded);
+ }
+}
+
+void nsCocoaWindow::ReportSizeEvent() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ UpdateBounds();
+ if (mWidgetListener) {
+ LayoutDeviceIntRect innerBounds = GetClientBounds();
+ mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::PauseCompositor() {
+ nsIWidget* mainChildView = static_cast<nsIWidget*>([[mWindow mainChildView] widget]);
+ if (!mainChildView) {
+ return;
+ }
+ CompositorBridgeChild* remoteRenderer = mainChildView->GetRemoteRenderer();
+ if (!remoteRenderer) {
+ return;
+ }
+ remoteRenderer->SendPause();
+}
+
+void nsCocoaWindow::ResumeCompositor() {
+ nsIWidget* mainChildView = static_cast<nsIWidget*>([[mWindow mainChildView] widget]);
+ if (!mainChildView) {
+ return;
+ }
+ CompositorBridgeChild* remoteRenderer = mainChildView->GetRemoteRenderer();
+ if (!remoteRenderer) {
+ return;
+ }
+ remoteRenderer->SendResume();
+}
+
+void nsCocoaWindow::SetMenuBar(RefPtr<nsMenuBarX>&& aMenuBar) {
+ if (!mWindow) {
+ mMenuBar = nullptr;
+ return;
+ }
+ mMenuBar = std::move(aMenuBar);
+
+ // Only paint for active windows, or paint the hidden window menu bar if no
+ // other menu bar has been painted yet so that some reasonable menu bar is
+ // displayed when the app starts up.
+ if (mMenuBar && ((!gSomeMenuBarPainted && nsMenuUtilsX::GetHiddenWindowMenuBar() == mMenuBar) ||
+ [mWindow isMainWindow]))
+ mMenuBar->Paint();
+}
+
+void nsCocoaWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) {
+ if (!mWindow) return;
+
+ if (mPopupContentView) {
+ return mPopupContentView->SetFocus(aRaise, aCallerType);
+ }
+
+ if (aRaise == Raise::Yes && ([mWindow isVisible] || [mWindow isMiniaturized])) {
+ if ([mWindow isMiniaturized]) {
+ [mWindow deminiaturize:nil];
+ }
+
+ [mWindow makeKeyAndOrderFront:nil];
+ SendSetZLevelEvent();
+ }
+}
+
+LayoutDeviceIntPoint nsCocoaWindow::WidgetToScreenOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return nsCocoaUtils::CocoaRectToGeckoRectDevPix(GetClientCocoaRect(), BackingScaleFactor())
+ .TopLeft();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+LayoutDeviceIntPoint nsCocoaWindow::GetClientOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ LayoutDeviceIntRect clientRect = GetClientBounds();
+
+ return clientRect.TopLeft() - mBounds.TopLeft();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+LayoutDeviceIntMargin nsCocoaWindow::ClientToWindowMargin() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mWindow || mWindow.drawsContentsIntoWindowFrame || mWindowType == WindowType::Popup) {
+ return {};
+ }
+
+ NSRect clientNSRect = mWindow.contentLayoutRect;
+ NSRect frameNSRect = [mWindow frameRectForChildViewRect:clientNSRect];
+
+ CGFloat backingScale = BackingScaleFactor();
+ const auto clientRect = nsCocoaUtils::CocoaRectToGeckoRectDevPix(clientNSRect, backingScale);
+ const auto frameRect = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frameNSRect, backingScale);
+
+ return frameRect - clientRect;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN({});
+}
+
+nsMenuBarX* nsCocoaWindow::GetMenuBar() { return mMenuBar; }
+
+void nsCocoaWindow::CaptureRollupEvents(bool aDoCapture) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aDoCapture) {
+ if (![NSApp isActive]) {
+ // We need to capture mouse event if we aren't
+ // the active application. We only set this up when needed
+ // because they cause spurious mouse event after crash
+ // and gdb sessions. See bug 699538.
+ nsToolkit::GetToolkit()->MonitorAllProcessMouseEvents();
+ }
+
+ // Sometimes more than one popup window can be visible at the same time
+ // (e.g. nested non-native context menus, or the test case (attachment
+ // 276885) for bmo bug 392389, which displays a non-native combo-box in a
+ // non-native popup window). In these cases the "active" popup window should
+ // be the topmost -- the (nested) context menu the mouse is currently over,
+ // or the combo-box's drop-down list (when it's displayed). But (among
+ // windows that have the same "level") OS X makes topmost the window that
+ // last received a mouse-down event, which may be incorrect (in the combo-
+ // box case, it makes topmost the window containing the combo-box). So
+ // here we fiddle with a non-native popup window's level to make sure the
+ // "active" one is always above any other non-native popup windows that
+ // may be visible.
+ if (mWindow && (mWindowType == WindowType::Popup)) SetPopupWindowLevel();
+ } else {
+ nsToolkit::GetToolkit()->StopMonitoringAllProcessMouseEvents();
+
+ // XXXndeakin this doesn't make sense.
+ // Why is the new window assumed to be a modal panel?
+ if (mWindow && (mWindowType == WindowType::Popup)) [mWindow setLevel:NSModalPanelWindowLevel];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsCocoaWindow::GetAttention(int32_t aCycleCount) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [NSApp requestUserAttention:NSInformationalRequest];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsCocoaWindow::HasPendingInputEvent() { return nsChildView::DoHasPendingInputEvent(); }
+
+void nsCocoaWindow::SetWindowShadowStyle(StyleWindowShadow aStyle) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mShadowStyle = aStyle;
+
+ if (!mWindow || mWindowType != WindowType::Popup) {
+ return;
+ }
+
+ mWindow.shadowStyle = mShadowStyle;
+ [mWindow setUseMenuStyle:mShadowStyle == StyleWindowShadow::Menu];
+ [mWindow setHasShadow:aStyle != StyleWindowShadow::None];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetWindowOpacity(float aOpacity) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ [mWindow setAlphaValue:(CGFloat)aOpacity];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetColorScheme(const Maybe<ColorScheme>& aScheme) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ mWindow.appearance = aScheme ? NSAppearanceForColorScheme(*aScheme) : nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static inline CGAffineTransform GfxMatrixToCGAffineTransform(const gfx::Matrix& m) {
+ CGAffineTransform t;
+ t.a = m._11;
+ t.b = m._12;
+ t.c = m._21;
+ t.d = m._22;
+ t.tx = m._31;
+ t.ty = m._32;
+ return t;
+}
+
+void nsCocoaWindow::SetWindowTransform(const gfx::Matrix& aTransform) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ // Calling CGSSetWindowTransform when the window is not visible results in
+ // misplacing the window into doubled x,y coordinates (see bug 1448132).
+ if (![mWindow isVisible] || NSIsEmptyRect([mWindow frame])) {
+ return;
+ }
+
+ if (StaticPrefs::widget_window_transforms_disabled()) {
+ // CGSSetWindowTransform is a private API. In case calling it causes
+ // problems either now or in the future, we'll want to have an easy kill
+ // switch. So we allow disabling it with a pref.
+ return;
+ }
+
+ gfx::Matrix transform = aTransform;
+
+ // aTransform is a transform that should be applied to the window relative
+ // to its regular position: If aTransform._31 is 100, then we want the
+ // window to be displayed 100 pixels to the right of its regular position.
+ // The transform that CGSSetWindowTransform accepts has a different meaning:
+ // It's used to answer the question "For the screen pixel at x,y (with the
+ // origin at the top left), what pixel in the window's buffer (again with
+ // origin top left) should be displayed at that position?"
+ // In the example above, this means that we need to call
+ // CGSSetWindowTransform with a horizontal translation of -windowPos.x - 100.
+ // So we need to invert the transform and adjust it by the window's position.
+ if (!transform.Invert()) {
+ // Treat non-invertible transforms as the identity transform.
+ transform = gfx::Matrix();
+ }
+
+ bool isIdentity = transform.IsIdentity();
+ if (isIdentity && mWindowTransformIsIdentity) {
+ return;
+ }
+
+ transform.PreTranslate(-mBounds.x, -mBounds.y);
+
+ // Snap translations to device pixels, to match what we do for CSS transforms
+ // and because the window server rounds down instead of to nearest.
+ if (!transform.HasNonTranslation() && transform.HasNonIntegerTranslation()) {
+ auto snappedTranslation = gfx::IntPoint::Round(transform.GetTranslation());
+ transform = gfx::Matrix::Translation(snappedTranslation.x, snappedTranslation.y);
+ }
+
+ // We also need to account for the backing scale factor: aTransform is given
+ // in device pixels, but CGSSetWindowTransform works with logical display
+ // pixels.
+ CGFloat backingScale = BackingScaleFactor();
+ transform.PreScale(backingScale, backingScale);
+ transform.PostScale(1 / backingScale, 1 / backingScale);
+
+ CGSConnection cid = _CGSDefaultConnection();
+ CGSSetWindowTransform(cid, [mWindow windowNumber], GfxMatrixToCGAffineTransform(transform));
+
+ mWindowTransformIsIdentity = isIdentity;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetInputRegion(const InputRegion& aInputRegion) {
+ MOZ_ASSERT(mWindowType == WindowType::Popup, "This should only be called on popup windows.");
+ // TODO: Somehow support aInputRegion.mMargin? Though maybe not.
+ if (aInputRegion.mFullyTransparent) {
+ [mWindow setIgnoresMouseEvents:YES];
+ } else {
+ [mWindow setIgnoresMouseEvents:NO];
+ }
+}
+
+void nsCocoaWindow::SetShowsToolbarButton(bool aShow) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) [mWindow setShowsToolbarButton:aShow];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSupportsNativeFullscreen(bool aSupportsNativeFullscreen) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) {
+ // This determines whether we tell cocoa that the window supports native
+ // full screen. If we do so, and another window is in native full screen,
+ // this window will also appear in native full screen. We generally only
+ // want to do this for primary application windows. We'll set the
+ // relevant macnativefullscreen attribute on those, which will lead to us
+ // being called with aSupportsNativeFullscreen set to `true` here.
+ NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior];
+ if (aSupportsNativeFullscreen) {
+ newBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
+ } else {
+ newBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
+ }
+ [mWindow setCollectionBehavior:newBehavior];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetWindowAnimationType(nsIWidget::WindowAnimationType aType) {
+ mAnimationType = aType;
+}
+
+void nsCocoaWindow::SetDrawsTitle(bool aDrawTitle) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (![mWindow drawsContentsIntoWindowFrame]) {
+ // If we don't draw into the window frame, we always want to display window
+ // titles.
+ [mWindow setWantsTitleDrawn:YES];
+ } else {
+ [mWindow setWantsTitleDrawn:aDrawTitle];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsCocoaWindow::SetNonClientMargins(const LayoutDeviceIntMargin& margins) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ SetDrawsInTitlebar(margins.top == 0);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::SetDrawsInTitlebar(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) {
+ [mWindow setDrawsContentsIntoWindowFrame:aState];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ if (mPopupContentView) {
+ return mPopupContentView->SynthesizeNativeMouseEvent(aPoint, aNativeMessage, aButton,
+ aModifierFlags, nullptr);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, double aDeltaY,
+ double aDeltaZ, uint32_t aModifierFlags, uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ if (mPopupContentView) {
+ // Pass nullptr as the observer so that the AutoObserverNotification in
+ // nsChildView::SynthesizeNativeMouseScrollEvent will be ignored.
+ return mPopupContentView->SynthesizeNativeMouseScrollEvent(aPoint, aNativeMessage, aDeltaX,
+ aDeltaY, aDeltaZ, aModifierFlags,
+ aAdditionalFlags, nullptr);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::LockAspectRatio(bool aShouldLock) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aShouldLock) {
+ [mWindow setContentAspectRatio:mWindow.frame.size];
+ mAspectRatioLocked = true;
+ } else {
+ // According to https://developer.apple.com/documentation/appkit/nswindow/1419507-aspectratio,
+ // aspect ratios and resize increments are mutually exclusive, and the accepted way of
+ // cancelling an established aspect ratio is to set the resize increments to 1.0, 1.0
+ [mWindow setResizeIncrements:NSMakeSize(1.0, 1.0)];
+ mAspectRatioLocked = false;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mPopupContentView) {
+ return mPopupContentView->UpdateThemeGeometries(aThemeGeometries);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetPopupWindowLevel() {
+ if (!mWindow) return;
+
+ // Floating popups are at the floating level and hide when the window is
+ // deactivated.
+ if (mPopupLevel == PopupLevel::Floating) {
+ [mWindow setLevel:NSFloatingWindowLevel];
+ [mWindow setHidesOnDeactivate:YES];
+ } else {
+ // Otherwise, this is a top-level or parent popup. Parent popups always
+ // appear just above their parent and essentially ignore the level.
+ [mWindow setLevel:NSPopUpMenuWindowLevel];
+ [mWindow setHidesOnDeactivate:NO];
+ }
+}
+
+void nsCocoaWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mInputContext = aContext;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool nsCocoaWindow::GetEditCommands(NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ // When the keyboard event is fired from this widget, it must mean that no web content has focus
+ // because any web contents should be on `nsChildView`. And in any locales, the system UI is
+ // always horizontal layout. So, let's pass `Nothing()` for the writing mode here, it won't be
+ // treated as in a vertical content.
+ keyBindings->GetEditCommands(aEvent, Nothing(), aCommands);
+ return true;
+}
+
+bool nsCocoaWindow::AsyncPanZoomEnabled() const {
+ if (mPopupContentView) {
+ return mPopupContentView->AsyncPanZoomEnabled();
+ }
+ return nsBaseWidget::AsyncPanZoomEnabled();
+}
+
+bool nsCocoaWindow::StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) {
+ if (mPopupContentView) {
+ return mPopupContentView->StartAsyncAutoscroll(aAnchorLocation, aGuid);
+ }
+ return nsBaseWidget::StartAsyncAutoscroll(aAnchorLocation, aGuid);
+}
+
+void nsCocoaWindow::StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) {
+ if (mPopupContentView) {
+ mPopupContentView->StopAsyncAutoscroll(aGuid);
+ return;
+ }
+ nsBaseWidget::StopAsyncAutoscroll(aGuid);
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsCocoaWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsChildView();
+ return window.forget();
+}
+
+@implementation WindowDelegate
+
+// We try to find a gecko menu bar to paint. If one does not exist, just paint
+// the application menu by itself so that a window doesn't have some other
+// window's menu bar.
++ (void)paintMenubarForWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // make sure we only act on windows that have this kind of
+ // object as a delegate
+ id windowDelegate = [aWindow delegate];
+ if ([windowDelegate class] != [self class]) return;
+
+ nsCocoaWindow* geckoWidget = [windowDelegate geckoWidget];
+ NS_ASSERTION(geckoWidget, "Window delegate not returning a gecko widget!");
+
+ nsMenuBarX* geckoMenuBar = geckoWidget->GetMenuBar();
+ if (geckoMenuBar) {
+ geckoMenuBar->Paint();
+ } else {
+ // sometimes we don't have a native application menu early in launching
+ if (!sApplicationMenu) return;
+
+ NSMenu* mainMenu = [NSApp mainMenu];
+ NS_ASSERTION([mainMenu numberOfItems] > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ // Create a new menu bar.
+ // We create a GeckoNSMenu because all menu bar NSMenu objects should use that subclass for
+ // key handling reasons.
+ GeckoNSMenu* newMenuBar = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
+
+ // move the application menu from the existing menu bar to the new one
+ NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain];
+ [mainMenu removeItemAtIndex:0];
+ [newMenuBar insertItem:firstMenuItem atIndex:0];
+ [firstMenuItem release];
+
+ // set our new menu bar as the main menu
+ [NSApp setMainMenu:newMenuBar];
+ [newMenuBar release];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [super init];
+ mGeckoWindow = geckoWind;
+ mToplevelActiveState = false;
+ mHasEverBeenZoomed = false;
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)proposedFrameSize {
+ RollUpPopups();
+ return proposedFrameSize;
+}
+
+- (NSRect)windowWillUseStandardFrame:(NSWindow*)window defaultFrame:(NSRect)newFrame {
+ // This function needs to return a rect representing the frame a window would
+ // have if it is in its "maximized" size mode. The parameter newFrame is supposed
+ // to be a frame representing the maximum window size on the screen where the
+ // window currently appears. However, in practice, newFrame can be a much smaller
+ // size. So, we ignore newframe and instead return the frame of the entire screen
+ // associated with the window. That frame is bigger than the window could actually
+ // be, due to the presence of the menubar and possibly the dock, but we never call
+ // this function directly, and Cocoa callers will shrink it to its true maximum
+ // size.
+ return window.screen.frame;
+}
+
+void nsCocoaWindow::CocoaSendToplevelActivateEvents() {
+ if (mWidgetListener) {
+ mWidgetListener->WindowActivated();
+ }
+}
+
+void nsCocoaWindow::CocoaSendToplevelDeactivateEvents() {
+ if (mWidgetListener) {
+ mWidgetListener->WindowDeactivated();
+ }
+}
+
+void nsCocoaWindow::CocoaWindowDidResize() {
+ // It's important to update our bounds before we trigger any listeners. This
+ // ensures that our bounds are correct when GetScreenBounds is called.
+ UpdateBounds();
+
+ if (HandleUpdateFullscreenOnResize()) {
+ ReportSizeEvent();
+ return;
+ }
+
+ // Resizing might have changed our zoom state.
+ DispatchSizeModeEvent();
+ ReportSizeEvent();
+}
+
+- (void)windowDidResize:(NSNotification*)aNotification {
+ BaseWindow* window = [aNotification object];
+ [window updateTrackingArea];
+
+ if (!mGeckoWindow) return;
+
+ mGeckoWindow->CocoaWindowDidResize();
+}
+
+- (void)windowDidChangeScreen:(NSNotification*)aNotification {
+ if (!mGeckoWindow) return;
+
+ // Because of Cocoa's peculiar treatment of zero-size windows (see comments
+ // at GetBackingScaleFactor() above), we sometimes have a situation where
+ // our concept of backing scale (based on the screen where the zero-sized
+ // window is positioned) differs from Cocoa's idea (always based on the
+ // Retina screen, AFAICT, even when an external non-Retina screen is the
+ // primary display).
+ //
+ // As a result, if the window was created with zero size on an external
+ // display, but then made visible on the (secondary) Retina screen, we
+ // will *not* get a windowDidChangeBackingProperties notification for it.
+ // This leads to an incorrect GetDefaultScale(), and widget coordinate
+ // confusion, as per bug 853252.
+ //
+ // To work around this, we check for a backing scale mismatch when we
+ // receive a windowDidChangeScreen notification, as we will receive this
+ // even if Cocoa was already treating the zero-size window as having
+ // Retina backing scale.
+ NSWindow* window = (NSWindow*)[aNotification object];
+ if ([window respondsToSelector:@selector(backingScaleFactor)]) {
+ if (GetBackingScaleFactor(window) != mGeckoWindow->BackingScaleFactor()) {
+ mGeckoWindow->BackingScaleFactorChanged();
+ }
+ }
+
+ mGeckoWindow->ReportMoveEvent();
+}
+
+- (void)windowWillEnterFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowWillEnterFullscreen(true);
+}
+
+// Lion's full screen mode will bypass our internal fullscreen tracking, so
+// we need to catch it when we transition and call our own methods, which in
+// turn will fire "fullscreen" events.
+- (void)windowDidEnterFullScreen:(NSNotification*)notification {
+ // On Yosemite, the NSThemeFrame class has two new properties --
+ // titlebarView (an NSTitlebarView object) and titlebarContainerView (an
+ // NSTitlebarContainerView object). These are used to display the titlebar
+ // in fullscreen mode. In Safari they're not transparent. But in Firefox
+ // for some reason they are, which causes bug 1069658. The following code
+ // works around this Apple bug or design flaw.
+ NSWindow* window = (NSWindow*)[notification object];
+ NSView* frameView = [[window contentView] superview];
+ NSView* titlebarView = nil;
+ NSView* titlebarContainerView = nil;
+ if ([frameView respondsToSelector:@selector(titlebarView)]) {
+ titlebarView = [frameView titlebarView];
+ }
+ if ([frameView respondsToSelector:@selector(titlebarContainerView)]) {
+ titlebarContainerView = [frameView titlebarContainerView];
+ }
+ if ([titlebarView respondsToSelector:@selector(setTransparent:)]) {
+ [titlebarView setTransparent:NO];
+ }
+ if ([titlebarContainerView respondsToSelector:@selector(setTransparent:)]) {
+ [titlebarContainerView setTransparent:NO];
+ }
+
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(true);
+}
+
+- (void)windowWillExitFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowWillEnterFullscreen(false);
+}
+
+- (void)windowDidExitFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(false);
+}
+
+- (void)windowDidFailToEnterFullScreen:(NSWindow*)window {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidFailFullscreen(true);
+}
+
+- (void)windowDidFailToExitFullScreen:(NSWindow*)window {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidFailFullscreen(false);
+}
+
+- (void)windowDidBecomeMain:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
+ // app modally. If one of those is up then we want it to retain its menu bar.
+ if ([NSApp _isRunningAppModal]) return;
+ NSWindow* window = [aNotification object];
+ if (window) [WindowDelegate paintMenubarForWindow:window];
+
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ [(ToolbarWindow*)window windowMainStateChanged];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowDidResignMain:(NSNotification*)aNotification {
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
+ // app modally. If one of those is up then we want it to retain its menu bar.
+ if ([NSApp _isRunningAppModal]) return;
+ RefPtr<nsMenuBarX> hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (hiddenWindowMenuBar) {
+ // printf("painting hidden window menu bar due to window losing main status\n");
+ hiddenWindowMenuBar->Paint();
+ }
+
+ NSWindow* window = [aNotification object];
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ [(ToolbarWindow*)window windowMainStateChanged];
+ }
+}
+
+- (void)windowDidBecomeKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ NSWindow* window = [aNotification object];
+ if ([window isSheet]) [WindowDelegate paintMenubarForWindow:window];
+
+ nsChildView* mainChildView =
+ static_cast<nsChildView*>([[(BaseWindow*)window mainChildView] widget]);
+ if (mainChildView) {
+ if (mainChildView->GetInputContext().IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowDidResignKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups(nsIRollupListener::AllowAnimations::No);
+
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // If a sheet just resigned key then we should paint the menu bar
+ // for whatever window is now main.
+ NSWindow* window = [aNotification object];
+ if ([window isSheet]) [WindowDelegate paintMenubarForWindow:[NSApp mainWindow]];
+
+ TextInputHandler::EnsureSecureEventInputDisabled();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowWillMove:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowDidMove:(NSNotification*)aNotification {
+ if (mGeckoWindow) mGeckoWindow->ReportMoveEvent();
+}
+
+- (BOOL)windowShouldClose:(id)sender {
+ nsIWidgetListener* listener = mGeckoWindow ? mGeckoWindow->GetWidgetListener() : nullptr;
+ if (listener) listener->RequestWindowClose(mGeckoWindow);
+ return NO; // gecko will do it
+}
+
+- (void)windowWillClose:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowWillMiniaturize:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowDidMiniaturize:(NSNotification*)aNotification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Miniaturize);
+}
+
+- (void)windowDidDeminiaturize:(NSNotification*)aNotification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Deminiaturize);
+}
+
+- (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)proposedFrame {
+ if (!mHasEverBeenZoomed && [window isZoomed]) return NO; // See bug 429954.
+
+ mHasEverBeenZoomed = YES;
+ return YES;
+}
+
+- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet usingRect:(NSRect)rect {
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ rect.origin.y = [(ToolbarWindow*)window sheetAttachmentPosition];
+ }
+ return rect;
+}
+
+#ifdef MOZ_THUNDERBIRD
+- (void)didEndSheet:(NSWindow*)sheet returnCode:(int)returnCode contextInfo:(void*)contextInfo {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Note: 'contextInfo' (if it is set) is the window that is the parent of
+ // the sheet. The value of contextInfo is determined in
+ // nsCocoaWindow::Show(). If it's set, 'contextInfo' is always the top-
+ // level window, not another sheet itself. But 'contextInfo' is nil if
+ // our parent window is also a sheet -- in that case we shouldn't send
+ // the top-level window any activate events (because it's our parent
+ // window that needs to get these events, not the top-level window).
+ [TopLevelWindowData deactivateInWindow:sheet];
+ [sheet orderOut:self];
+ if (contextInfo) [TopLevelWindowData activateInWindow:(NSWindow*)contextInfo];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+#endif
+
+- (void)windowDidChangeBackingProperties:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSWindow* window = (NSWindow*)[aNotification object];
+
+ if ([window respondsToSelector:@selector(backingScaleFactor)]) {
+ CGFloat oldFactor =
+ [[[aNotification userInfo] objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue];
+ if ([window backingScaleFactor] != oldFactor) {
+ mGeckoWindow->BackingScaleFactorChanged();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// This method is on NSWindowDelegate starting with 10.9
+- (void)windowDidChangeOcclusionState:(NSNotification*)aNotification {
+ if (mGeckoWindow) {
+ mGeckoWindow->DispatchOcclusionEvent();
+ }
+}
+
+- (nsCocoaWindow*)geckoWidget {
+ return mGeckoWindow;
+}
+
+- (bool)toplevelActiveState {
+ return mToplevelActiveState;
+}
+
+- (void)sendToplevelActivateEvents {
+ if (!mToplevelActiveState && mGeckoWindow) {
+ mGeckoWindow->CocoaSendToplevelActivateEvents();
+
+ mToplevelActiveState = true;
+ }
+}
+
+- (void)sendToplevelDeactivateEvents {
+ if (mToplevelActiveState && mGeckoWindow) {
+ mGeckoWindow->CocoaSendToplevelDeactivateEvents();
+ mToplevelActiveState = false;
+ }
+}
+
+@end
+
+@interface NSView (FrameViewMethodSwizzling)
+- (NSPoint)FrameView__closeButtonOrigin;
+- (CGFloat)FrameView__titlebarHeight;
+@end
+
+@implementation NSView (FrameViewMethodSwizzling)
+
+- (NSPoint)FrameView__closeButtonOrigin {
+ if (![self.window isKindOfClass:[ToolbarWindow class]]) {
+ return self.FrameView__closeButtonOrigin;
+ }
+ ToolbarWindow* win = (ToolbarWindow*)[self window];
+ if (win.drawsContentsIntoWindowFrame && !(win.styleMask & NSWindowStyleMaskFullScreen) &&
+ (win.styleMask & NSWindowStyleMaskTitled)) {
+ const NSRect buttonsRect = win.windowButtonsRect;
+ if (NSIsEmptyRect(buttonsRect)) {
+ // Empty rect. Let's hide the buttons.
+ // Position is in non-flipped window coordinates. Using frame's height
+ // for the vertical coordinate will move the buttons above the window,
+ // making them invisible.
+ return NSMakePoint(buttonsRect.origin.x, win.frame.size.height);
+ } else if (win.windowTitlebarLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft) {
+ // We're in RTL mode, which means that the close button is the rightmost
+ // button of the three window buttons. and buttonsRect.origin is the
+ // bottom left corner of the green (zoom) button. The close button is 40px
+ // to the right of the zoom button. This is confirmed to be the same on
+ // all macOS versions between 10.12 - 12.0.
+ return NSMakePoint(buttonsRect.origin.x + 40.0f, buttonsRect.origin.y);
+ }
+ return buttonsRect.origin;
+ }
+ return self.FrameView__closeButtonOrigin;
+}
+
+- (CGFloat)FrameView__titlebarHeight {
+ CGFloat height = [self FrameView__titlebarHeight];
+ if ([[self window] isKindOfClass:[ToolbarWindow class]]) {
+ // Make sure that the titlebar height includes our shifted buttons.
+ // The following coordinates are in window space, with the origin being at the bottom left
+ // corner of the window.
+ ToolbarWindow* win = (ToolbarWindow*)[self window];
+ CGFloat frameHeight = [self frame].size.height;
+ CGFloat windowButtonY = frameHeight;
+ if (!NSIsEmptyRect(win.windowButtonsRect) && win.drawsContentsIntoWindowFrame &&
+ !(win.styleMask & NSWindowStyleMaskFullScreen) &&
+ (win.styleMask & NSWindowStyleMaskTitled)) {
+ windowButtonY = win.windowButtonsRect.origin.y;
+ }
+ height = std::max(height, frameHeight - windowButtonY);
+ }
+ return height;
+}
+
+@end
+
+static NSMutableSet* gSwizzledFrameViewClasses = nil;
+
+@interface NSWindow (PrivateSetNeedsDisplayInRectMethod)
+- (void)_setNeedsDisplayInRect:(NSRect)aRect;
+@end
+
+@interface NSView (NSVisualEffectViewSetMaskImage)
+- (void)setMaskImage:(NSImage*)image;
+@end
+
+@interface BaseWindow (Private)
+- (void)removeTrackingArea;
+- (void)cursorUpdated:(NSEvent*)aEvent;
+- (void)reflowTitlebarElements;
+@end
+
+@implementation BaseWindow
+
+// The frame of a window is implemented using undocumented NSView subclasses.
+// We offset the window buttons by overriding the method _closeButtonOrigin on
+// these frame view classes. The class which is
+// used for a window is determined in the window's frameViewClassForStyleMask:
+// method, so this is where we make sure that we have swizzled the method on
+// all encountered classes.
++ (Class)frameViewClassForStyleMask:(NSUInteger)styleMask {
+ Class frameViewClass = [super frameViewClassForStyleMask:styleMask];
+
+ if (!gSwizzledFrameViewClasses) {
+ gSwizzledFrameViewClasses = [[NSMutableSet setWithCapacity:3] retain];
+ if (!gSwizzledFrameViewClasses) {
+ return frameViewClass;
+ }
+ }
+
+ static IMP our_closeButtonOrigin =
+ class_getMethodImplementation([NSView class], @selector(FrameView__closeButtonOrigin));
+ static IMP our_titlebarHeight =
+ class_getMethodImplementation([NSView class], @selector(FrameView__titlebarHeight));
+
+ if (![gSwizzledFrameViewClasses containsObject:frameViewClass]) {
+ // Either of these methods might be implemented in both a subclass of
+ // NSFrameView and one of its own subclasses. Which means that if we
+ // aren't careful we might end up swizzling the same method twice.
+ // Since method swizzling involves swapping pointers, this would break
+ // things.
+ IMP _closeButtonOrigin =
+ class_getMethodImplementation(frameViewClass, @selector(_closeButtonOrigin));
+ if (_closeButtonOrigin && _closeButtonOrigin != our_closeButtonOrigin) {
+ nsToolkit::SwizzleMethods(frameViewClass, @selector(_closeButtonOrigin),
+ @selector(FrameView__closeButtonOrigin));
+ }
+
+ // Override _titlebarHeight so that the floating titlebar doesn't clip the bottom of the
+ // window buttons which we move down with our override of _closeButtonOrigin.
+ IMP _titlebarHeight = class_getMethodImplementation(frameViewClass, @selector(_titlebarHeight));
+ if (_titlebarHeight && _titlebarHeight != our_titlebarHeight) {
+ nsToolkit::SwizzleMethods(frameViewClass, @selector(_titlebarHeight),
+ @selector(FrameView__titlebarHeight));
+ }
+
+ [gSwizzledFrameViewClasses addObject:frameViewClass];
+ }
+
+ return frameViewClass;
+}
+
+- (id)initWithContentRect:(NSRect)aContentRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)aBufferingType
+ defer:(BOOL)aFlag {
+ mDrawsIntoWindowFrame = NO;
+ [super initWithContentRect:aContentRect styleMask:aStyle backing:aBufferingType defer:aFlag];
+ mState = nil;
+ mDisabledNeedsDisplay = NO;
+ mTrackingArea = nil;
+ mDirtyRect = NSZeroRect;
+ mBeingShown = NO;
+ mDrawTitle = NO;
+ mUseMenuStyle = NO;
+ mTouchBar = nil;
+ mIsAnimationSuppressed = NO;
+ [self updateTrackingArea];
+
+ return self;
+}
+
+// Returns an autoreleased NSImage.
+static NSImage* GetMenuMaskImage() {
+ CGFloat radius = 4.0f;
+ NSEdgeInsets insets = {5, 5, 5, 5};
+ NSSize maskSize = {12, 12};
+ NSImage* maskImage = [NSImage imageWithSize:maskSize
+ flipped:YES
+ drawingHandler:^BOOL(NSRect dstRect) {
+ NSBezierPath* path =
+ [NSBezierPath bezierPathWithRoundedRect:dstRect
+ xRadius:radius
+ yRadius:radius];
+ [[NSColor colorWithDeviceWhite:1.0 alpha:1.0] set];
+ [path fill];
+ return YES;
+ }];
+ [maskImage setCapInsets:insets];
+ return maskImage;
+}
+
+- (void)swapOutChildViewWrapper:(NSView*)aNewWrapper {
+ [aNewWrapper setFrame:[[self contentView] frame]];
+ NSView* childView = [[self mainChildView] retain];
+ [childView removeFromSuperview];
+ [aNewWrapper addSubview:childView];
+ [childView release];
+ [super setContentView:aNewWrapper];
+}
+
+- (void)setUseMenuStyle:(BOOL)aValue {
+ if (aValue && !mUseMenuStyle) {
+ // Turn on rounded corner masking.
+ NSView* effectView = VibrancyManager::CreateEffectView(VibrancyType::MENU, YES);
+ [effectView setMaskImage:GetMenuMaskImage()];
+ [self swapOutChildViewWrapper:effectView];
+ [effectView release];
+ } else if (mUseMenuStyle && !aValue) {
+ // Turn off rounded corner masking.
+ NSView* wrapper = [[NSView alloc] initWithFrame:NSZeroRect];
+ [wrapper setWantsLayer:YES];
+ [self swapOutChildViewWrapper:wrapper];
+ [wrapper release];
+ }
+ mUseMenuStyle = aValue;
+}
+
+- (NSTouchBar*)makeTouchBar {
+ mTouchBar = [[nsTouchBar alloc] init];
+ if (mTouchBar) {
+ sTouchBarIsInitialized = YES;
+ }
+ return mTouchBar;
+}
+
+- (void)setBeingShown:(BOOL)aValue {
+ mBeingShown = aValue;
+}
+
+- (BOOL)isBeingShown {
+ return mBeingShown;
+}
+
+- (BOOL)isVisibleOrBeingShown {
+ return [super isVisible] || mBeingShown;
+}
+
+- (void)setIsAnimationSuppressed:(BOOL)aValue {
+ mIsAnimationSuppressed = aValue;
+}
+
+- (BOOL)isAnimationSuppressed {
+ return mIsAnimationSuppressed;
+}
+
+- (void)disableSetNeedsDisplay {
+ mDisabledNeedsDisplay = YES;
+}
+
+- (void)enableSetNeedsDisplay {
+ mDisabledNeedsDisplay = NO;
+}
+
+- (void)dealloc {
+ [mTouchBar release];
+ [self removeTrackingArea];
+ ChildViewMouseTracker::OnDestroyWindow(self);
+ [super dealloc];
+}
+
+static const NSString* kStateTitleKey = @"title";
+static const NSString* kStateDrawsContentsIntoWindowFrameKey = @"drawsContentsIntoWindowFrame";
+static const NSString* kStateShowsToolbarButton = @"showsToolbarButton";
+static const NSString* kStateCollectionBehavior = @"collectionBehavior";
+static const NSString* kStateWantsTitleDrawn = @"wantsTitleDrawn";
+
+- (void)importState:(NSDictionary*)aState {
+ if (NSString* title = [aState objectForKey:kStateTitleKey]) {
+ [self setTitle:title];
+ }
+ [self setDrawsContentsIntoWindowFrame:[[aState objectForKey:kStateDrawsContentsIntoWindowFrameKey]
+ boolValue]];
+ [self setShowsToolbarButton:[[aState objectForKey:kStateShowsToolbarButton] boolValue]];
+ [self setCollectionBehavior:[[aState objectForKey:kStateCollectionBehavior] unsignedIntValue]];
+ [self setWantsTitleDrawn:[[aState objectForKey:kStateWantsTitleDrawn] boolValue]];
+}
+
+- (NSMutableDictionary*)exportState {
+ NSMutableDictionary* state = [NSMutableDictionary dictionaryWithCapacity:10];
+ if (NSString* title = [self title]) {
+ [state setObject:title forKey:kStateTitleKey];
+ }
+ [state setObject:[NSNumber numberWithBool:[self drawsContentsIntoWindowFrame]]
+ forKey:kStateDrawsContentsIntoWindowFrameKey];
+ [state setObject:[NSNumber numberWithBool:[self showsToolbarButton]]
+ forKey:kStateShowsToolbarButton];
+ [state setObject:[NSNumber numberWithUnsignedInt:[self collectionBehavior]]
+ forKey:kStateCollectionBehavior];
+ [state setObject:[NSNumber numberWithBool:[self wantsTitleDrawn]] forKey:kStateWantsTitleDrawn];
+ return state;
+}
+
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState {
+ bool changed = (aState != mDrawsIntoWindowFrame);
+ mDrawsIntoWindowFrame = aState;
+ if (changed) {
+ [self reflowTitlebarElements];
+ }
+}
+
+- (BOOL)drawsContentsIntoWindowFrame {
+ return mDrawsIntoWindowFrame;
+}
+
+- (NSRect)childViewRectForFrameRect:(NSRect)aFrameRect {
+ if (mDrawsIntoWindowFrame) {
+ return aFrameRect;
+ }
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ return [NSWindow contentRectForFrameRect:aFrameRect styleMask:styleMask];
+}
+
+- (NSRect)frameRectForChildViewRect:(NSRect)aChildViewRect {
+ if (mDrawsIntoWindowFrame) {
+ return aChildViewRect;
+ }
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ return [NSWindow frameRectForContentRect:aChildViewRect styleMask:styleMask];
+}
+
+- (NSTimeInterval)animationResizeTime:(NSRect)newFrame {
+ if (mIsAnimationSuppressed) {
+ // Should not animate the initial session-restore size change
+ return 0.0;
+ }
+
+ return [super animationResizeTime:newFrame];
+}
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
+ mDrawTitle = aDrawTitle;
+ [self setTitleVisibility:mDrawTitle ? NSWindowTitleVisible : NSWindowTitleHidden];
+}
+
+- (BOOL)wantsTitleDrawn {
+ return mDrawTitle;
+}
+
+- (NSView*)trackingAreaView {
+ NSView* contentView = [self contentView];
+ return [contentView superview] ? [contentView superview] : contentView;
+}
+
+- (NSArray<NSView*>*)contentViewContents {
+ return [[[[self contentView] subviews] copy] autorelease];
+}
+
+- (ChildView*)mainChildView {
+ NSView* contentView = [self contentView];
+ NSView* lastView = [[contentView subviews] lastObject];
+ if ([lastView isKindOfClass:[ChildView class]]) {
+ return (ChildView*)lastView;
+ }
+ return nil;
+}
+
+- (void)removeTrackingArea {
+ if (mTrackingArea) {
+ [[self trackingAreaView] removeTrackingArea:mTrackingArea];
+ [mTrackingArea release];
+ mTrackingArea = nil;
+ }
+}
+
+- (void)updateTrackingArea {
+ [self removeTrackingArea];
+
+ NSView* view = [self trackingAreaView];
+ const NSTrackingAreaOptions options =
+ NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways;
+ mTrackingArea = [[NSTrackingArea alloc] initWithRect:[view bounds]
+ options:options
+ owner:self
+ userInfo:nil];
+ [view addTrackingArea:mTrackingArea];
+}
+
+- (void)mouseEntered:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseEnteredWindow(aEvent);
+}
+
+- (void)mouseExited:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseExitedWindow(aEvent);
+}
+
+- (void)mouseMoved:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseMoved(aEvent);
+}
+
+- (void)cursorUpdated:(NSEvent*)aEvent {
+ // Nothing to do here, but NSTrackingArea wants us to implement this method.
+}
+
+- (void)_setNeedsDisplayInRect:(NSRect)aRect {
+ // Prevent unnecessary invalidations due to moving NSViews (e.g. for plugins)
+ if (!mDisabledNeedsDisplay) {
+ // This method is only called by Cocoa, so when we're here, we know that
+ // it's available and don't need to check whether our superclass responds
+ // to the selector.
+ [super _setNeedsDisplayInRect:aRect];
+ mDirtyRect = NSUnionRect(mDirtyRect, aRect);
+ }
+}
+
+- (NSRect)getAndResetNativeDirtyRect {
+ NSRect dirtyRect = mDirtyRect;
+ mDirtyRect = NSZeroRect;
+ return dirtyRect;
+}
+
+// Possibly move the titlebar buttons.
+- (void)reflowTitlebarElements {
+ NSView* frameView = [[self contentView] superview];
+ if ([frameView respondsToSelector:@selector(_tileTitlebarAndRedisplay:)]) {
+ [frameView _tileTitlebarAndRedisplay:NO];
+ }
+}
+
+- (BOOL)respondsToSelector:(SEL)aSelector {
+ // Claim the window doesn't respond to this so that the system
+ // doesn't steal keyboard equivalents for it. Bug 613710.
+ if (aSelector == @selector(cancelOperation:)) {
+ return NO;
+ }
+
+ return [super respondsToSelector:aSelector];
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ // We override this so that it won't beep if it can't act.
+ // We want to control the beeping for missing or disabled
+ // commands ourselves.
+ [self tryToPerform:aSelector with:nil];
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ id retval = [super accessibilityAttributeValue:attribute];
+
+ // The following works around a problem with Text-to-Speech on OS X 10.7.
+ // See bug 674612 for more info.
+ //
+ // When accessibility is off, AXUIElementCopyAttributeValue(), when called
+ // on an AXApplication object to get its AXFocusedUIElement attribute,
+ // always returns an AXWindow object (the actual browser window -- never a
+ // mozAccessible object). This also happens with accessibility turned on,
+ // if no other object in the browser window has yet been focused. But if
+ // the browser window has a title bar (as it currently always does), the
+ // AXWindow object will always have four "accessible" children, one of which
+ // is an AXStaticText object (the title bar's "title"; the other three are
+ // the close, minimize and zoom buttons). This means that (for complicated
+ // reasons, for which see bug 674612) Text-to-Speech on OS X 10.7 will often
+ // "speak" the window title, no matter what text is selected, or even if no
+ // text at all is selected. (This always happens when accessibility is off.
+ // It doesn't happen in Firefox releases because Apple has (on OS X 10.7)
+ // special-cased the handling of apps whose CFBundleIdentifier is
+ // org.mozilla.firefox.)
+ //
+ // We work around this problem by only returning AXChildren that are
+ // mozAccessible object or are one of the titlebar's buttons (which
+ // instantiate subclasses of NSButtonCell).
+ if ([retval isKindOfClass:[NSArray class]] && [attribute isEqualToString:@"AXChildren"]) {
+ NSMutableArray* holder = [NSMutableArray arrayWithCapacity:10];
+ [holder addObjectsFromArray:(NSArray*)retval];
+ NSUInteger count = [holder count];
+ for (NSInteger i = count - 1; i >= 0; --i) {
+ id item = [holder objectAtIndex:i];
+ // Remove anything from holder that isn't one of the titlebar's buttons
+ // (which instantiate subclasses of NSButtonCell) or a mozAccessible
+ // object (or one of its subclasses).
+ if (![item isKindOfClass:[NSButtonCell class]] &&
+ ![item respondsToSelector:@selector(hasRepresentedView)]) {
+ [holder removeObjectAtIndex:i];
+ }
+ }
+ retval = [NSArray arrayWithArray:holder];
+ }
+
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)releaseJSObjects {
+ [mTouchBar releaseJSObjects];
+}
+
+@end
+
+@interface NSView (NSThemeFrame)
+- (void)_drawTitleStringInClip:(NSRect)aRect;
+- (void)_maskCorners:(NSUInteger)aFlags clipRect:(NSRect)aRect;
+@end
+
+@implementation MOZTitlebarView
+
+- (instancetype)initWithFrame:(NSRect)aFrame {
+ self = [super initWithFrame:aFrame];
+
+ self.material = NSVisualEffectMaterialTitlebar;
+ self.blendingMode = NSVisualEffectBlendingModeWithinWindow;
+
+ // Add a separator line at the bottom of the titlebar. NSBoxSeparator isn't a perfect match for
+ // a native titlebar separator, but it's better than nothing.
+ // We really want the appearance that _NSTitlebarDecorationView creates with the help of CoreUI,
+ // but there's no public API for that.
+ NSBox* separatorLine = [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, aFrame.size.width, 1)];
+ separatorLine.autoresizingMask = NSViewWidthSizable | NSViewMaxYMargin;
+ separatorLine.boxType = NSBoxSeparator;
+ [self addSubview:separatorLine];
+ [separatorLine release];
+
+ return self;
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ return YES;
+}
+
+- (void)mouseUp:(NSEvent*)event {
+ if ([event clickCount] == 2) {
+ // Handle titlebar double click. We don't get the window's default behavior here because the
+ // window uses NSWindowStyleMaskFullSizeContentView, and this view (the titlebar gradient view)
+ // is technically part of the window "contents" (it's a subview of the content view).
+ if (nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick()) {
+ [[self window] performZoom:nil];
+ } else if (nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick()) {
+ [[self window] performMiniaturize:nil];
+ }
+ }
+}
+
+@end
+
+@interface MOZTitlebarAccessoryView : NSView
+@end
+
+@implementation MOZTitlebarAccessoryView : NSView
+- (void)viewWillMoveToWindow:(NSWindow*)aWindow {
+ if (aWindow) {
+ // When entering full screen mode, titlebar accessory views are inserted
+ // into a floating NSWindow which houses the window titlebar and toolbars.
+ // In order to work around a drawing bug with titlebarAppearsTransparent
+ // windows in full screen mode, disable titlebar separators for all
+ // NSWindows that this view is used in, including the floating full screen
+ // toolbar window. The drawing bug was filed as FB9056136. See bug 1700211
+ // for more details.
+#if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+#else
+ if (@available(macOS 11.0, *)) {
+#endif
+ aWindow.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone;
+ }
+ }
+}
+@end
+
+@implementation FullscreenTitlebarTracker
+- (FullscreenTitlebarTracker*)init {
+ [super init];
+ self.view = [[[MOZTitlebarAccessoryView alloc] initWithFrame:NSZeroRect] autorelease];
+ self.hidden = YES;
+ return self;
+}
+@end
+
+// This class allows us to exercise control over the window's title bar. It is
+// used for all windows with titlebars.
+//
+// ToolbarWindow supports two modes:
+// - drawsContentsIntoWindowFrame mode: In this mode, the Gecko ChildView is
+// sized to cover the entire window frame and manages titlebar drawing.
+// - separate titlebar mode, with support for unified toolbars: In this mode,
+// the Gecko ChildView does not extend into the titlebar. However, this
+// window's content view (which is the ChildView's superview) *does* extend
+// into the titlebar. Moreover, in this mode, we place a MOZTitlebarView
+// in the content view, as a sibling of the ChildView.
+//
+// The "separate titlebar mode" supports the "unified toolbar" look:
+// If there's a toolbar right below the titlebar, the two can "connect" and
+// form a single gradient without a separator line in between.
+//
+// The following mechanism communicates the height of the unified toolbar to
+// the ToolbarWindow:
+//
+// 1) In the style sheet we set the toolbar's -moz-appearance to toolbar.
+// 2) When the toolbar is visible and we paint the application chrome
+// window, the array that Gecko passes nsChildView::UpdateThemeGeometries
+// will contain an entry for the widget type StyleAppearance::Toolbar.
+// 3) nsChildView::UpdateThemeGeometries passes the toolbar's height, plus the
+// titlebar height, to -[ToolbarWindow setUnifiedToolbarHeight:].
+//
+// The actual drawing of the gradient happens in two parts: The titlebar part
+// (i.e. the top 22 pixels of the gradient) is drawn by the MOZTitlebarView,
+// which is a subview of the window's content view and a sibling of the ChildView.
+// The rest of the gradient is drawn by Gecko into the ChildView, as part of the
+// -moz-appearance rendering of the toolbar.
+@implementation ToolbarWindow
+
+- (id)initWithContentRect:(NSRect)aChildViewRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)aBufferingType
+ defer:(BOOL)aFlag {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We treat aChildViewRect as the rectangle that the window's main ChildView
+ // should be sized to. Get the right frameRect for the requested child view
+ // rect.
+ NSRect frameRect = [NSWindow frameRectForContentRect:aChildViewRect styleMask:aStyle];
+
+ // Always size the content view to the full frame size of the window.
+ // We do this even if we want this window to have a titlebar; in that case, the window's content
+ // view covers the entire window but the ChildView inside it will only cover the content area. We
+ // do this so that we can render the titlebar gradient manually, with a subview of our content
+ // view that's positioned in the titlebar area. This lets us have a smooth connection between
+ // titlebar and toolbar gradient in case the window has a "unified toolbar + titlebar" look.
+ // Moreover, always using a full size content view lets us toggle the titlebar on and off without
+ // changing the window's style mask (which would have other subtle effects, for example on
+ // keyboard focus).
+ aStyle |= NSWindowStyleMaskFullSizeContentView;
+
+ // -[NSWindow initWithContentRect:styleMask:backing:defer:] calls
+ // [self frameRectForContentRect:styleMask:] to convert the supplied content
+ // rect to the window's frame rect. We've overridden that method to be a
+ // pass-through function. So, in order to get the intended frameRect, we need
+ // to supply frameRect itself as the "content rect".
+ NSRect contentRect = frameRect;
+
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:aStyle
+ backing:aBufferingType
+ defer:aFlag])) {
+ mTitlebarView = nil;
+ mUnifiedToolbarHeight = 22.0f;
+ mSheetAttachmentPosition = aChildViewRect.size.height;
+ mWindowButtonsRect = NSZeroRect;
+ mInitialTitlebarHeight = [self titlebarHeight];
+
+ [self setTitlebarAppearsTransparent:YES];
+#if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+#else
+ if (@available(macOS 11.0, *)) {
+#endif
+ self.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone;
+ }
+ [self updateTitlebarView];
+
+ mFullscreenTitlebarTracker = [[FullscreenTitlebarTracker alloc] init];
+ // revealAmount is an undocumented property of
+ // NSTitlebarAccessoryViewController that updates whenever the menubar
+ // slides down in fullscreen mode.
+ [mFullscreenTitlebarTracker addObserver:self
+ forKeyPath:@"revealAmount"
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+ // Adding this accessory view controller allows us to shift the toolbar down
+ // when the user mouses to the top of the screen in fullscreen.
+ [(NSWindow*)self addTitlebarAccessoryViewController:mFullscreenTitlebarTracker];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id>*)change
+ context:(void*)context {
+ if ([keyPath isEqualToString:@"revealAmount"]) {
+ [[self mainChildView] ensureNextCompositeIsAtomicWithMainThreadPaint];
+ NSNumber* revealAmount = (change[NSKeyValueChangeNewKey]);
+ [self updateTitlebarShownAmount:[revealAmount doubleValue]];
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+static bool ScreenHasNotch(nsCocoaWindow* aGeckoWindow) {
+ if (@available(macOS 12.0, *)) {
+ nsCOMPtr<nsIScreen> widgetScreen = aGeckoWindow->GetWidgetScreen();
+ NSScreen* cocoaScreen = ScreenHelperCocoa::CocoaScreenForScreen(widgetScreen);
+ return cocoaScreen.safeAreaInsets.top != 0.0f;
+ }
+ return false;
+}
+
+static bool ShouldShiftByMenubarHeightInFullscreen(nsCocoaWindow* aWindow) {
+ switch (StaticPrefs::widget_macos_shift_by_menubar_on_fullscreen()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ break;
+ }
+ // TODO: On notch-less macbooks, this creates extra space when the
+ // "automatically show and hide the menubar on fullscreen" option is unchecked
+ // (default checked). We tried to detect that in bug 1737831 but it wasn't
+ // reliable enough, see the regressions from that bug. For now, stick to the
+ // good behavior for default configurations (that is, shift by menubar height
+ // on notch-less macbooks, and don't for devices that have a notch). This will
+ // need refinement in the future.
+ return !ScreenHasNotch(aWindow);
+}
+
+- (void)updateTitlebarShownAmount:(CGFloat)aShownAmount {
+ NSInteger styleMask = [self styleMask];
+ if (!(styleMask & NSWindowStyleMaskFullScreen)) {
+ // We are not interested in the size of the titlebar unless we are in
+ // fullscreen.
+ return;
+ }
+
+ // [NSApp mainMenu] menuBarHeight] returns one of two values: the full height
+ // if the menubar is shown or is in the process of being shown, and 0
+ // otherwise. Since we are multiplying the menubar height by aShownAmount, we
+ // always want the full height.
+ CGFloat menuBarHeight = NSApp.mainMenu.menuBarHeight;
+ if (menuBarHeight > 0.0f) {
+ mMenuBarHeight = menuBarHeight;
+ }
+ if ([[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (!geckoWindow) {
+ return;
+ }
+
+ if (nsIWidgetListener* listener = geckoWindow->GetWidgetListener()) {
+ // Use the titlebar height cached in our frame rather than
+ // [ToolbarWindow titlebarHeight]. titlebarHeight returns 0 when we're in
+ // fullscreen.
+ CGFloat shiftByPixels = mInitialTitlebarHeight * aShownAmount;
+ if (ShouldShiftByMenubarHeightInFullscreen(geckoWindow)) {
+ shiftByPixels += mMenuBarHeight * aShownAmount;
+ }
+ // Use mozilla::DesktopToLayoutDeviceScale rather than the
+ // DesktopToLayoutDeviceScale in nsCocoaWindow. The latter accounts for
+ // screen DPI. We don't want that because the revealAmount property
+ // already accounts for it, so we'd be compounding DPI scales > 1.
+ mozilla::DesktopCoord coord =
+ LayoutDeviceCoord(shiftByPixels) / mozilla::DesktopToLayoutDeviceScale();
+
+ listener->MacFullscreenMenubarOverlapChanged(coord);
+ }
+ }
+}
+
+- (void)dealloc {
+ [mTitlebarView release];
+ [mFullscreenTitlebarTracker removeObserver:self forKeyPath:@"revealAmount"];
+ [mFullscreenTitlebarTracker removeFromParentViewController];
+ [mFullscreenTitlebarTracker release];
+
+ [super dealloc];
+}
+
+- (NSArray<NSView*>*)contentViewContents {
+ NSMutableArray<NSView*>* contents = [[[self contentView] subviews] mutableCopy];
+ if (mTitlebarView) {
+ // Do not include the titlebar gradient view in the returned array.
+ [contents removeObject:mTitlebarView];
+ }
+ return [contents autorelease];
+}
+
+- (void)updateTitlebarView {
+ BOOL needTitlebarView = ![self drawsContentsIntoWindowFrame] || mUnifiedToolbarHeight > 0;
+ if (needTitlebarView && !mTitlebarView) {
+ mTitlebarView = [[MOZTitlebarView alloc] initWithFrame:[self unifiedToolbarRect]];
+ mTitlebarView.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
+ [self.contentView addSubview:mTitlebarView positioned:NSWindowBelow relativeTo:nil];
+ } else if (needTitlebarView && mTitlebarView) {
+ mTitlebarView.frame = [self unifiedToolbarRect];
+ } else if (!needTitlebarView && mTitlebarView) {
+ [mTitlebarView removeFromSuperview];
+ [mTitlebarView release];
+ mTitlebarView = nil;
+ }
+}
+
+- (void)windowMainStateChanged {
+ [self setTitlebarNeedsDisplay];
+ [[self mainChildView] ensureNextCompositeIsAtomicWithMainThreadPaint];
+}
+
+- (void)setTitlebarNeedsDisplay {
+ [mTitlebarView setNeedsDisplay:YES];
+}
+
+- (NSRect)titlebarRect {
+ CGFloat titlebarHeight = [self titlebarHeight];
+ return NSMakeRect(0, [self frame].size.height - titlebarHeight, [self frame].size.width,
+ titlebarHeight);
+}
+
+// In window contentView coordinates (origin bottom left)
+- (NSRect)unifiedToolbarRect {
+ return NSMakeRect(0, [self frame].size.height - mUnifiedToolbarHeight, [self frame].size.width,
+ mUnifiedToolbarHeight);
+}
+
+// Returns the unified height of titlebar + toolbar.
+- (CGFloat)unifiedToolbarHeight {
+ return mUnifiedToolbarHeight;
+}
+
+- (CGFloat)titlebarHeight {
+ // We use the original content rect here, not what we return from
+ // [self contentRectForFrameRect:], because that would give us a
+ // titlebarHeight of zero.
+ NSRect frameRect = [self frame];
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ NSRect originalContentRect = [NSWindow contentRectForFrameRect:frameRect styleMask:styleMask];
+ return NSMaxY(frameRect) - NSMaxY(originalContentRect);
+}
+
+// Stores the complete height of titlebar + toolbar.
+- (void)setUnifiedToolbarHeight:(CGFloat)aHeight {
+ if (aHeight == mUnifiedToolbarHeight) return;
+
+ mUnifiedToolbarHeight = aHeight;
+
+ [self updateTitlebarView];
+}
+
+// Extending the content area into the title bar works by resizing the
+// mainChildView so that it covers the titlebar.
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState {
+ BOOL stateChanged = ([self drawsContentsIntoWindowFrame] != aState);
+ [super setDrawsContentsIntoWindowFrame:aState];
+ if (stateChanged && [[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ // Here we extend / shrink our mainChildView. We do that by firing a resize
+ // event which will cause the ChildView to be resized to the rect returned
+ // by nsCocoaWindow::GetClientBounds. GetClientBounds bases its return
+ // value on what we return from drawsContentsIntoWindowFrame.
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (geckoWindow) {
+ // Re-layout our contents.
+ geckoWindow->ReportSizeEvent();
+ }
+
+ // Resizing the content area causes a reflow which would send a synthesized
+ // mousemove event to the old mouse position relative to the top left
+ // corner of the content area. But the mouse has shifted relative to the
+ // content area, so that event would have wrong position information. So
+ // we'll send a mouse move event with the correct new position.
+ ChildViewMouseTracker::ResendLastMouseMoveEvent();
+ }
+
+ [self updateTitlebarView];
+}
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
+ [super setWantsTitleDrawn:aDrawTitle];
+ [self setTitlebarNeedsDisplay];
+}
+
+- (void)setSheetAttachmentPosition:(CGFloat)aY {
+ mSheetAttachmentPosition = aY;
+}
+
+- (CGFloat)sheetAttachmentPosition {
+ return mSheetAttachmentPosition;
+}
+
+- (void)placeWindowButtons:(NSRect)aRect {
+ if (!NSEqualRects(mWindowButtonsRect, aRect)) {
+ mWindowButtonsRect = aRect;
+ [self reflowTitlebarElements];
+ }
+}
+
+- (NSRect)windowButtonsRect {
+ return mWindowButtonsRect;
+}
+
+// Returning YES here makes the setShowsToolbarButton method work even though
+// the window doesn't contain an NSToolbar.
+- (BOOL)_hasToolbar {
+ return YES;
+}
+
+// Dispatch a toolbar pill button clicked message to Gecko.
+- (void)_toolbarPillButtonClicked:(id)sender {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+
+ if ([[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (!geckoWindow) return;
+
+ nsIWidgetListener* listener = geckoWindow->GetWidgetListener();
+ if (listener) listener->OSToolbarButtonPressed();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Retain and release "self" to avoid crashes when our widget (and its native
+// window) is closed as a result of processing a key equivalent (e.g.
+// Command+w or Command+q). This workaround is only needed for a window
+// that can become key.
+- (BOOL)performKeyEquivalent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* nativeWindow = [self retain];
+ BOOL retval = [super performKeyEquivalent:theEvent];
+ [nativeWindow release];
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ NSEventType type = [anEvent type];
+
+ switch (type) {
+ case NSEventTypeScrollWheel:
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeMouseMoved:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDragged: {
+ // Drop all mouse events if a modal window has appeared above us.
+ // This helps make us behave as if the OS were running a "real" modal
+ // event loop.
+ id delegate = [self delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) {
+ nsCocoaWindow* widget = [(WindowDelegate*)delegate geckoWidget];
+ if (widget) {
+ if (gGeckoAppModalWindowList && (widget != gGeckoAppModalWindowList->window)) return;
+ if (widget->HasModalDescendents()) return;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ [super sendEvent:anEvent];
+}
+
+@end
+
+@implementation PopupWindow
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)styleMask
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)deferCreation {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ mIsContextMenu = false;
+ return [super initWithContentRect:contentRect
+ styleMask:styleMask
+ backing:bufferingType
+ defer:deferCreation];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Override the private API _backdropBleedAmount. This determines how much the
+// desktop wallpaper contributes to the vibrancy backdrop.
+// Return 0 in order to match what the system does for sheet windows and
+// _NSPopoverWindows.
+- (CGFloat)_backdropBleedAmount {
+ return 0.0;
+}
+
+// Override the private API shadowOptions.
+// The constants below were found in AppKit's implementations of the
+// shadowOptions method on the various window types.
+static const NSUInteger kWindowShadowOptionsNoShadow = 0;
+static const NSUInteger kWindowShadowOptionsMenu = 2;
+static const NSUInteger kWindowShadowOptionsTooltipMojaveOrLater = 4;
+- (NSUInteger)shadowOptions {
+ if (!self.hasShadow) {
+ return kWindowShadowOptionsNoShadow;
+ }
+
+ switch (self.shadowStyle) {
+ case StyleWindowShadow::None:
+ return kWindowShadowOptionsNoShadow;
+
+ case StyleWindowShadow::Default: // we treat "default" as "default panel"
+ case StyleWindowShadow::Menu:
+ return kWindowShadowOptionsMenu;
+
+ case StyleWindowShadow::Tooltip:
+ if (nsCocoaFeatures::OnMojaveOrLater()) {
+ return kWindowShadowOptionsTooltipMojaveOrLater;
+ }
+ return kWindowShadowOptionsMenu;
+ }
+}
+
+- (BOOL)isContextMenu {
+ return mIsContextMenu;
+}
+
+- (void)setIsContextMenu:(BOOL)flag {
+ mIsContextMenu = flag;
+}
+
+- (BOOL)canBecomeMainWindow {
+ // This is overriden because the default is 'yes' when a titlebar is present.
+ return NO;
+}
+
+@end
+
+// According to Apple's docs on [NSWindow canBecomeKeyWindow] and [NSWindow
+// canBecomeMainWindow], windows without a title bar or resize bar can't (by
+// default) become key or main. But if a window can't become key, it can't
+// accept keyboard input (bmo bug 393250). And it should also be possible for
+// an otherwise "ordinary" window to become main. We need to override these
+// two methods to make this happen.
+@implementation BorderlessWindow
+
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ NSEventType type = [anEvent type];
+
+ switch (type) {
+ case NSEventTypeScrollWheel:
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeMouseMoved:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDragged: {
+ // Drop all mouse events if a modal window has appeared above us.
+ // This helps make us behave as if the OS were running a "real" modal
+ // event loop.
+ id delegate = [self delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) {
+ nsCocoaWindow* widget = [(WindowDelegate*)delegate geckoWidget];
+ if (widget) {
+ if (gGeckoAppModalWindowList && (widget != gGeckoAppModalWindowList->window)) return;
+ if (widget->HasModalDescendents()) return;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ [super sendEvent:anEvent];
+}
+
+// Apple's doc on this method says that the NSWindow class's default is not to
+// become main if the window isn't "visible" -- so we should replicate that
+// behavior here. As best I can tell, the [NSWindow isVisible] method is an
+// accurate test of what Apple means by "visibility".
+- (BOOL)canBecomeMainWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (![self isVisible]) return NO;
+ return YES;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+// Retain and release "self" to avoid crashes when our widget (and its native
+// window) is closed as a result of processing a key equivalent (e.g.
+// Command+w or Command+q). This workaround is only needed for a window
+// that can become key.
+- (BOOL)performKeyEquivalent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* nativeWindow = [self retain];
+ BOOL retval = [super performKeyEquivalent:theEvent];
+ [nativeWindow release];
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+@end
diff --git a/widget/cocoa/nsColorPicker.h b/widget/cocoa/nsColorPicker.h
new file mode 100644
index 0000000000..325f159fde
--- /dev/null
+++ b/widget/cocoa/nsColorPicker.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsColorPicker_h_
+#define nsColorPicker_h_
+
+#include "nsIColorPicker.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+class nsIColorPickerShownCallback;
+class mozIDOMWindowProxy;
+@class NSColorPanelWrapper;
+@class NSColor;
+
+class nsColorPicker final : public nsIColorPicker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOLORPICKER
+
+ // For NSColorPanelWrapper.
+ void Update(NSColor* aColor);
+ void Done();
+
+ private:
+ ~nsColorPicker();
+
+ static NSColor* GetNSColorFromHexString(const nsAString& aColor);
+ static void GetHexStringFromNSColor(NSColor* aColor, nsAString& aResult);
+
+ NSColorPanelWrapper* mColorPanelWrapper;
+
+ nsString mTitle;
+ nsString mColor;
+ nsCOMPtr<nsIColorPickerShownCallback> mCallback;
+};
+
+#endif // nsColorPicker_h_
diff --git a/widget/cocoa/nsColorPicker.mm b/widget/cocoa/nsColorPicker.mm
new file mode 100644
index 0000000000..255610b27c
--- /dev/null
+++ b/widget/cocoa/nsColorPicker.mm
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 2; 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 <Cocoa/Cocoa.h>
+
+#include "nsColorPicker.h"
+#include "nsCocoaUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+
+static unsigned int HexStrToInt(NSString* str) {
+ unsigned int result = 0;
+
+ for (unsigned int i = 0; i < [str length]; ++i) {
+ char c = [str characterAtIndex:i];
+ result *= 16;
+ if (c >= '0' && c <= '9') {
+ result += c - '0';
+ } else if (c >= 'A' && c <= 'F') {
+ result += 10 + (c - 'A');
+ } else {
+ result += 10 + (c - 'a');
+ }
+ }
+
+ return result;
+}
+
+@interface NSColorPanelWrapper : NSObject <NSWindowDelegate> {
+ NSColorPanel* mColorPanel;
+ nsColorPicker* mColorPicker;
+}
+- (id)initWithPicker:(nsColorPicker*)aPicker;
+- (void)open:(NSColor*)aInitialColor title:(NSString*)aTitle;
+- (void)colorChanged:(NSColorPanel*)aPanel;
+- (void)windowWillClose:(NSNotification*)aNotification;
+- (void)close;
+@end
+
+@implementation NSColorPanelWrapper
+- (id)initWithPicker:(nsColorPicker*)aPicker {
+ mColorPicker = aPicker;
+ mColorPanel = [NSColorPanel sharedColorPanel];
+
+ self = [super init];
+ return self;
+}
+
+- (void)open:(NSColor*)aInitialColor title:(NSString*)aTitle {
+ [mColorPanel setTarget:self];
+ [mColorPanel setAction:@selector(colorChanged:)];
+ [mColorPanel setDelegate:self];
+ [mColorPanel setTitle:aTitle];
+ [mColorPanel setColor:aInitialColor];
+ [mColorPanel setFrameOrigin:[NSEvent mouseLocation]];
+ [mColorPanel makeKeyAndOrderFront:nil];
+}
+
+- (void)colorChanged:(NSColorPanel*)aPanel {
+ if (!mColorPicker) {
+ return;
+ }
+ mColorPicker->Update([mColorPanel color]);
+}
+
+- (void)windowWillClose:(NSNotification*)aNotification {
+ if (!mColorPicker) {
+ return;
+ }
+ mColorPicker->Done();
+}
+
+- (void)close {
+ [mColorPanel setTarget:nil];
+ [mColorPanel setAction:nil];
+ [mColorPanel setDelegate:nil];
+
+ mColorPanel = nil;
+ mColorPicker = nullptr;
+}
+@end
+
+NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker)
+
+nsColorPicker::~nsColorPicker() {
+ if (mColorPanelWrapper) {
+ [mColorPanelWrapper close];
+ [mColorPanelWrapper release];
+ mColorPanelWrapper = nullptr;
+ }
+}
+
+// TODO(bug 1805397): Implement default colors
+NS_IMETHODIMP
+nsColorPicker::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ const nsAString& aInitialColor, const nsTArray<nsString>& aDefaultColors) {
+ MOZ_ASSERT(NS_IsMainThread(), "Color pickers can only be opened from main thread currently");
+ mTitle = aTitle;
+ mColor = aInitialColor;
+ mColorPanelWrapper = [[NSColorPanelWrapper alloc] initWithPicker:this];
+ return NS_OK;
+}
+
+/* static */ NSColor* nsColorPicker::GetNSColorFromHexString(const nsAString& aColor) {
+ NSString* str = nsCocoaUtils::ToNSString(aColor);
+
+ double red = HexStrToInt([str substringWithRange:NSMakeRange(1, 2)]) / 255.0;
+ double green = HexStrToInt([str substringWithRange:NSMakeRange(3, 2)]) / 255.0;
+ double blue = HexStrToInt([str substringWithRange:NSMakeRange(5, 2)]) / 255.0;
+
+ return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0];
+}
+
+/* static */ void nsColorPicker::GetHexStringFromNSColor(NSColor* aColor, nsAString& aResult) {
+ CGFloat redFloat, greenFloat, blueFloat;
+
+ NSColor* color = aColor;
+ @try {
+ [color getRed:&redFloat green:&greenFloat blue:&blueFloat alpha:nil];
+ } @catch (NSException* e) {
+ color = [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
+ [color getRed:&redFloat green:&greenFloat blue:&blueFloat alpha:nil];
+ }
+
+ nsCocoaUtils::GetStringForNSString(
+ [NSString stringWithFormat:@"#%02x%02x%02x", (int)(redFloat * 255), (int)(greenFloat * 255),
+ (int)(blueFloat * 255)],
+ aResult);
+}
+
+NS_IMETHODIMP
+nsColorPicker::Open(nsIColorPickerShownCallback* aCallback) {
+ MOZ_ASSERT(aCallback);
+ mCallback = aCallback;
+
+ [mColorPanelWrapper open:GetNSColorFromHexString(mColor) title:nsCocoaUtils::ToNSString(mTitle)];
+
+ NS_ADDREF_THIS();
+
+ return NS_OK;
+}
+
+void nsColorPicker::Update(NSColor* aColor) {
+ GetHexStringFromNSColor(aColor, mColor);
+ mCallback->Update(mColor);
+}
+
+void nsColorPicker::Done() {
+ [mColorPanelWrapper close];
+ [mColorPanelWrapper release];
+ mColorPanelWrapper = nullptr;
+ mCallback->Done(u""_ns);
+ mCallback = nullptr;
+ NS_RELEASE_THIS();
+}
diff --git a/widget/cocoa/nsCursorManager.h b/widget/cocoa/nsCursorManager.h
new file mode 100644
index 0000000000..c36d8962fb
--- /dev/null
+++ b/widget/cocoa/nsCursorManager.h
@@ -0,0 +1,60 @@
+/* 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/. */
+
+#ifndef nsCursorManager_h_
+#define nsCursorManager_h_
+
+#import <Foundation/Foundation.h>
+#include "nsIWidget.h"
+#include "nsMacCursor.h"
+
+/*! @class nsCursorManager
+ @abstract Singleton service provides access to all cursors available in the application.
+ @discussion Use <code>nsCusorManager</code> to set the current cursor using
+ an XP <code>nsCusor</code> enum value.
+ <code>nsCursorManager</code> encapsulates the details of
+ setting different types of cursors, animating cursors and
+ cleaning up cursors when they are no longer in use.
+ */
+@interface nsCursorManager : NSObject {
+ @private
+ NSMutableDictionary* mCursors;
+ nsMacCursor* mCurrentMacCursor;
+}
+
+/*! @method setCursor:
+ @abstract Sets the current cursor.
+ @discussion Sets the current cursor to the cursor indicated by the XP
+ cursor given in the argument. Resources associated with the
+ previous cursor are cleaned up.
+ @param aCursor the cursor to use
+*/
+- (nsresult)setNonCustomCursor:(const nsIWidget::Cursor&)aCursor;
+
+// As above, but returns an error if the cursor isn't custom or we couldn't set
+// it for some reason.
+- (nsresult)setCustomCursor:(const nsIWidget::Cursor&)aCursor
+ widgetScaleFactor:(CGFloat)aWidgetScaleFactor;
+
+/*! @method sharedInstance
+ @abstract Get the Singleton instance of the cursor manager.
+ @discussion Use this method to obtain a reference to the cursor manager.
+ @result a reference to the cursor manager
+*/
++ (nsCursorManager*)sharedInstance;
+
+/*! @method dispose
+ @abstract Releases the shared instance of the cursor manager.
+ @discussion Use dispose to clean up the cursor manager and associated cursors.
+*/
++ (void)dispose;
+@end
+
+@interface NSCursor (Undocumented)
+// busyButClickableCursor is an undocumented NSCursor API, but has been in use since
+// at least OS X 10.4 and through 10.9.
++ (NSCursor*)busyButClickableCursor;
+@end
+
+#endif // nsCursorManager_h_
diff --git a/widget/cocoa/nsCursorManager.mm b/widget/cocoa/nsCursorManager.mm
new file mode 100644
index 0000000000..6596df25a3
--- /dev/null
+++ b/widget/cocoa/nsCursorManager.mm
@@ -0,0 +1,317 @@
+/* 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/. */
+
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+#include "nsCursorManager.h"
+#include "nsObjCExceptions.h"
+#include <math.h>
+
+static nsCursorManager* gInstance;
+static CGFloat sCurrentCursorScaleFactor = 0.0f;
+static nsIWidget::Cursor sCurrentCursor;
+static constexpr nsCursor kCustomCursor = eCursorCount;
+
+/*! @category nsCursorManager(PrivateMethods)
+ Private methods for the cursor manager class.
+*/
+@interface nsCursorManager (PrivateMethods)
+/*! @method getCursor:
+ @abstract Get a reference to the native Mac representation of a cursor.
+ @discussion Gets a reference to the Mac native implementation of a cursor.
+ If the cursor has been requested before, it is retreived from the cursor cache,
+ otherwise it is created and cached.
+ @param aCursor the cursor to get
+ @result the Mac native implementation of the cursor
+*/
+- (nsMacCursor*)getCursor:(nsCursor)aCursor;
+
+/*! @method setMacCursor:
+ @abstract Set the current Mac native cursor
+ @discussion Sets the current cursor - this routine is what actually causes the cursor to change.
+ The argument is retained and the old cursor is released.
+ @param aMacCursor the cursor to set
+ @result NS_OK
+ */
+- (nsresult)setMacCursor:(nsMacCursor*)aMacCursor;
+
+/*! @method createCursor:
+ @abstract Create a Mac native representation of a cursor.
+ @discussion Creates a version of the Mac native representation of this cursor
+ @param aCursor the cursor to create
+ @result the Mac native implementation of the cursor
+*/
++ (nsMacCursor*)createCursor:(enum nsCursor)aCursor;
+
+@end
+
+@implementation nsCursorManager
+
++ (nsCursorManager*)sharedInstance {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!gInstance) {
+ gInstance = [[nsCursorManager alloc] init];
+ }
+ return gInstance;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (void)dispose {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [gInstance release];
+ gInstance = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
++ (nsMacCursor*)createCursor:(enum nsCursor)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ switch (aCursor) {
+ case eCursor_standard:
+ return [nsMacCursor cursorWithCursor:[NSCursor arrowCursor] type:aCursor];
+ case eCursor_wait:
+ case eCursor_spinning: {
+ return [nsMacCursor cursorWithCursor:[NSCursor busyButClickableCursor] type:aCursor];
+ }
+ case eCursor_select:
+ return [nsMacCursor cursorWithCursor:[NSCursor IBeamCursor] type:aCursor];
+ case eCursor_hyperlink:
+ return [nsMacCursor cursorWithCursor:[NSCursor pointingHandCursor] type:aCursor];
+ case eCursor_crosshair:
+ return [nsMacCursor cursorWithCursor:[NSCursor crosshairCursor] type:aCursor];
+ case eCursor_move:
+ return [nsMacCursor cursorWithImageNamed:@"move" hotSpot:NSMakePoint(12, 12) type:aCursor];
+ case eCursor_help:
+ return [nsMacCursor cursorWithImageNamed:@"help" hotSpot:NSMakePoint(12, 12) type:aCursor];
+ case eCursor_copy: {
+ SEL cursorSelector = @selector(dragCopyCursor);
+ return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_alias: {
+ SEL cursorSelector = @selector(dragLinkCursor);
+ return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_context_menu: {
+ SEL cursorSelector = @selector(contextualMenuCursor);
+ return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_cell:
+ return [nsMacCursor cursorWithImageNamed:@"cell" hotSpot:NSMakePoint(12, 12) type:aCursor];
+ case eCursor_grab:
+ return [nsMacCursor cursorWithCursor:[NSCursor openHandCursor] type:aCursor];
+ case eCursor_grabbing:
+ return [nsMacCursor cursorWithCursor:[NSCursor closedHandCursor] type:aCursor];
+ case eCursor_zoom_in:
+ return [nsMacCursor cursorWithImageNamed:@"zoomIn" hotSpot:NSMakePoint(10, 10) type:aCursor];
+ case eCursor_zoom_out:
+ return [nsMacCursor cursorWithImageNamed:@"zoomOut" hotSpot:NSMakePoint(10, 10) type:aCursor];
+ case eCursor_vertical_text:
+ return [nsMacCursor cursorWithImageNamed:@"vtIBeam" hotSpot:NSMakePoint(12, 11) type:aCursor];
+ case eCursor_all_scroll:
+ return [nsMacCursor cursorWithCursor:[NSCursor openHandCursor] type:aCursor];
+ case eCursor_not_allowed:
+ case eCursor_no_drop: {
+ SEL cursorSelector = @selector(operationNotAllowedCursor);
+ return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ // Resize Cursors:
+ // North
+ case eCursor_n_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeUpCursor] type:aCursor];
+ // North East
+ case eCursor_ne_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNE" hotSpot:NSMakePoint(12, 11) type:aCursor];
+ // East
+ case eCursor_e_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeRightCursor] type:aCursor];
+ // South East
+ case eCursor_se_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeSE" hotSpot:NSMakePoint(12, 12) type:aCursor];
+ // South
+ case eCursor_s_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeDownCursor] type:aCursor];
+ // South West
+ case eCursor_sw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeSW" hotSpot:NSMakePoint(10, 12) type:aCursor];
+ // West
+ case eCursor_w_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeLeftCursor] type:aCursor];
+ // North West
+ case eCursor_nw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNW" hotSpot:NSMakePoint(11, 11) type:aCursor];
+ // North & South
+ case eCursor_ns_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeUpDownCursor] type:aCursor];
+ // East & West
+ case eCursor_ew_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeLeftRightCursor] type:aCursor];
+ // North East & South West
+ case eCursor_nesw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNESW"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // North West & South East
+ case eCursor_nwse_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNWSE"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // Column Resize
+ case eCursor_col_resize:
+ return [nsMacCursor cursorWithImageNamed:@"colResize"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // Row Resize
+ case eCursor_row_resize:
+ return [nsMacCursor cursorWithImageNamed:@"rowResize"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ default:
+ return [nsMacCursor cursorWithCursor:[NSCursor arrowCursor] type:aCursor];
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)init {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ mCursors = [[NSMutableDictionary alloc] initWithCapacity:25];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (nsresult)setNonCustomCursor:(const nsIWidget::Cursor&)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [self setMacCursor:[self getCursor:aCursor.mDefaultCursor]];
+
+ sCurrentCursor = aCursor;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsresult)setMacCursor:(nsMacCursor*)aMacCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCursor oldType = [mCurrentMacCursor type];
+ nsCursor newType = [aMacCursor type];
+ if (oldType != newType) {
+ if (newType == eCursor_none) {
+ [NSCursor hide];
+ } else if (oldType == eCursor_none) {
+ [NSCursor unhide];
+ }
+ }
+
+ if (mCurrentMacCursor != aMacCursor || ![mCurrentMacCursor isSet]) {
+ [aMacCursor retain];
+ [mCurrentMacCursor unset];
+ [aMacCursor set];
+ [mCurrentMacCursor release];
+ mCurrentMacCursor = aMacCursor;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsresult)setCustomCursor:(const nsIWidget::Cursor&)aCursor
+ widgetScaleFactor:(CGFloat)scaleFactor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // As the user moves the mouse, this gets called repeatedly with the same aCursorImage
+ if (sCurrentCursor == aCursor && sCurrentCursorScaleFactor == scaleFactor && mCurrentMacCursor) {
+ // Native dragging can unset our cursor apparently (see bug 1739352).
+ if (MOZ_UNLIKELY(![mCurrentMacCursor isSet])) {
+ [mCurrentMacCursor set];
+ }
+ return NS_OK;
+ }
+
+ sCurrentCursor = aCursor;
+ sCurrentCursorScaleFactor = scaleFactor;
+
+ if (!aCursor.IsCustom()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIntSize size = nsIWidget::CustomCursorSize(aCursor);
+ // prevent DoS attacks
+ if (size.width > 128 || size.height > 128) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSImage* cursorImage;
+ nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(
+ aCursor.mContainer, imgIContainer::FRAME_FIRST, nullptr, nullptr, &cursorImage, scaleFactor);
+ if (NS_FAILED(rv) || !cursorImage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ NSSize cocoaSize = NSMakeSize(size.width, size.height);
+ [cursorImage setSize:cocoaSize];
+ [[[cursorImage representations] objectAtIndex:0] setSize:cocoaSize];
+ }
+
+ // if the hotspot is nonsensical, make it 0,0
+ uint32_t hotspotX = aCursor.mHotspotX > (uint32_t(size.width) - 1) ? 0 : aCursor.mHotspotX;
+ uint32_t hotspotY = aCursor.mHotspotY > (uint32_t(size.height) - 1) ? 0 : aCursor.mHotspotY;
+ NSPoint hotSpot = ::NSMakePoint(hotspotX, hotspotY);
+ [self setMacCursor:[nsMacCursor cursorWithCursor:[[NSCursor alloc] initWithImage:cursorImage
+ hotSpot:hotSpot]
+ type:kCustomCursor]];
+ [cursorImage release];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsMacCursor*)getCursor:(enum nsCursor)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsMacCursor* result = [mCursors objectForKey:[NSNumber numberWithInt:aCursor]];
+ if (!result) {
+ result = [nsCursorManager createCursor:aCursor];
+ [mCursors setObject:result forKey:[NSNumber numberWithInt:aCursor]];
+ }
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mCurrentMacCursor unset];
+ [mCurrentMacCursor release];
+ [mCursors release];
+ sCurrentCursor = {};
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/nsDeviceContextSpecX.h b/widget/cocoa/nsDeviceContextSpecX.h
new file mode 100644
index 0000000000..9f44446c7d
--- /dev/null
+++ b/widget/cocoa/nsDeviceContextSpecX.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsDeviceContextSpecX_h_
+#define nsDeviceContextSpecX_h_
+
+#include "nsIDeviceContextSpec.h"
+#include "nsIPrinter.h"
+#include "nsIPrinterList.h"
+
+#include "nsCOMPtr.h"
+
+#include "mozilla/gfx/PrintPromise.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+
+class nsDeviceContextSpecX : public nsIDeviceContextSpec {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsDeviceContextSpecX();
+
+ NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override;
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) override;
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override;
+ NS_IMETHOD BeginPage() override { return NS_OK; };
+ NS_IMETHOD EndPage() override { return NS_OK; };
+
+ void GetPaperRect(double* aTop, double* aLeft, double* aBottom,
+ double* aRight);
+
+ protected:
+ virtual ~nsDeviceContextSpecX();
+
+ protected:
+ PMPrintSession mPrintSession = nullptr;
+ PMPageFormat mPageFormat = nullptr;
+ PMPrintSettings mPMPrintSettings = nullptr;
+ nsCOMPtr<nsIOutputStream> mOutputStream; // Output stream from settings.
+#ifdef MOZ_ENABLE_SKIA_PDF
+ // file "print" output generated if printing via PDF
+ nsCOMPtr<nsIFile> mTempFile;
+#endif
+ private:
+ nsresult DoEndDocument();
+};
+
+#endif // nsDeviceContextSpecX_h_
diff --git a/widget/cocoa/nsDeviceContextSpecX.mm b/widget/cocoa/nsDeviceContextSpecX.mm
new file mode 100644
index 0000000000..568eec44dc
--- /dev/null
+++ b/widget/cocoa/nsDeviceContextSpecX.mm
@@ -0,0 +1,303 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#include "nsDeviceContextSpecX.h"
+
+#import <Cocoa/Cocoa.h>
+#include "mozilla/gfx/PrintPromise.h"
+#include <CoreFoundation/CoreFoundation.h>
+#include <unistd.h>
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+# include "mozilla/gfx/PrintTargetSkPDF.h"
+#endif
+#include "mozilla/gfx/PrintTargetCG.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+
+#include "AppleUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsCRT.h"
+#include "nsCUPSShim.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsILocalFileMac.h"
+#include "nsIOutputStream.h"
+#include "nsPaper.h"
+#include "nsPrinterListCUPS.h"
+#include "nsPrintSettingsX.h"
+#include "nsQueryObject.h"
+#include "prenv.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::gfx::IntSize;
+using mozilla::gfx::PrintEndDocumentPromise;
+using mozilla::gfx::PrintTarget;
+using mozilla::gfx::PrintTargetCG;
+#ifdef MOZ_ENABLE_SKIA_PDF
+using mozilla::gfx::PrintTargetSkPDF;
+#endif
+
+//----------------------------------------------------------------------
+// nsDeviceContentSpecX
+
+nsDeviceContextSpecX::nsDeviceContextSpecX() = default;
+
+nsDeviceContextSpecX::~nsDeviceContextSpecX() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mPrintSession) {
+ ::PMRelease(mPrintSession);
+ }
+ if (mPageFormat) {
+ ::PMRelease(mPageFormat);
+ }
+ if (mPMPrintSettings) {
+ ::PMRelease(mPMPrintSettings);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecX, nsIDeviceContextSpec)
+
+NS_IMETHODIMP nsDeviceContextSpecX::Init(nsIPrintSettings* aPS, bool aIsPrintPreview) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ RefPtr<nsPrintSettingsX> settings(do_QueryObject(aPS));
+ if (!settings) {
+ return NS_ERROR_NO_INTERFACE;
+ }
+ // Note: unlike other platforms, we don't set our base class's mPrintSettings
+ // here since we don't need it currently (we do set mPMPrintSettings below).
+
+ NSPrintInfo* printInfo = settings->CreateOrCopyPrintInfo();
+ if (!printInfo) {
+ return NS_ERROR_FAILURE;
+ }
+ if (aPS->GetOutputDestination() == nsIPrintSettings::kOutputDestinationStream) {
+ aPS->GetOutputStream(getter_AddRefs(mOutputStream));
+ if (!mOutputStream) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ mPrintSession = static_cast<PMPrintSession>([printInfo PMPrintSession]);
+ mPageFormat = static_cast<PMPageFormat>([printInfo PMPageFormat]);
+ mPMPrintSettings = static_cast<PMPrintSettings>([printInfo PMPrintSettings]);
+ MOZ_ASSERT(mPrintSession && mPageFormat && mPMPrintSettings);
+ ::PMRetain(mPrintSession);
+ ::PMRetain(mPageFormat);
+ ::PMRetain(mPMPrintSettings);
+ [printInfo release];
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ nsAutoString printViaPdf;
+ mozilla::Preferences::GetString("print.print_via_pdf_encoder", printViaPdf);
+ if (printViaPdf.EqualsLiteral("skia-pdf")) {
+ // Annoyingly, PMPrinterPrintWithFile does not pay attention to the
+ // kPMDestination* value set in the PMPrintSession; it always sends the PDF
+ // to the specified printer. This means that if we create the PDF using
+ // SkPDF then we need to manually handle user actions like "Open PDF in
+ // Preview" and "Save as PDF...".
+ // TODO: Currently we do not support using SkPDF for kPMDestinationFax or
+ // kPMDestinationProcessPDF ("Add PDF to iBooks, etc.), and we only support
+ // it for kPMDestinationFile if the destination file is a PDF.
+ // XXX Could PMWorkflowSubmitPDFWithSettings/PMPrinterPrintWithProvider help?
+ OSStatus status = noErr;
+ PMDestinationType destination;
+ status = ::PMSessionGetDestinationType(mPrintSession, mPMPrintSettings, &destination);
+ if (status == noErr) {
+ if (destination == kPMDestinationPrinter || destination == kPMDestinationPreview) {
+ mPrintViaSkPDF = true;
+ } else if (destination == kPMDestinationFile) {
+ AutoCFRelease<CFURLRef> destURL(nullptr);
+ status =
+ ::PMSessionCopyDestinationLocation(mPrintSession, mPMPrintSettings, destURL.receive());
+ if (status == noErr) {
+ AutoCFRelease<CFStringRef> destPathRef =
+ CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
+ NSString* destPath = (NSString*)CFStringRef(destPathRef);
+ NSString* destPathExt = [destPath pathExtension];
+ if ([destPathExt isEqualToString:@"pdf"]) {
+ mPrintViaSkPDF = true;
+ }
+ }
+ }
+ }
+ }
+#endif
+
+ int16_t outputFormat = aPS->GetOutputFormat();
+
+ if (outputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ // We don't actually currently support/use kOutputFormatPDF on mac, but
+ // this is for completeness in case we add that (we probably need to in
+ // order to support adding links into saved PDFs, for example).
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, u"pdf_file"_ns, 1);
+ } else {
+ PMDestinationType destination;
+ OSStatus status = ::PMSessionGetDestinationType(mPrintSession, mPMPrintSettings, &destination);
+ if (status == noErr &&
+ (destination == kPMDestinationFile || destination == kPMDestinationPreview ||
+ destination == kPMDestinationProcessPDF)) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, u"pdf_file"_ns, 1);
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, u"unknown"_ns, 1);
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecX::EndDocument() {
+ return nsIDeviceContextSpec::EndDocumentPromiseFromResult(DoEndDocument(), __func__);
+}
+
+nsresult nsDeviceContextSpecX::DoEndDocument() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ OSStatus status = noErr;
+
+ nsCOMPtr<nsILocalFileMac> tmpPDFFile = do_QueryInterface(mTempFile);
+ if (!tmpPDFFile) {
+ return NS_ERROR_FAILURE;
+ }
+ AutoCFRelease<CFURLRef> pdfURL(nullptr);
+ // Note that the caller is responsible to release pdfURL according to nsILocalFileMac.idl,
+ // even though we didn't follow the Core Foundation naming conventions here (the method
+ // should've been called CopyCFURL).
+ nsresult rv = tmpPDFFile->GetCFURL(pdfURL.receive());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PMDestinationType destination;
+ status = ::PMSessionGetDestinationType(mPrintSession, mPMPrintSettings, &destination);
+
+ switch (destination) {
+ case kPMDestinationPrinter: {
+ PMPrinter currentPrinter = NULL;
+ status = ::PMSessionGetCurrentPrinter(mPrintSession, &currentPrinter);
+ if (status != noErr) {
+ return NS_ERROR_FAILURE;
+ }
+ CFStringRef mimeType = CFSTR("application/pdf");
+ status = ::PMPrinterPrintWithFile(currentPrinter, mPMPrintSettings, mPageFormat, mimeType,
+ pdfURL);
+ break;
+ }
+ case kPMDestinationPreview: {
+ // XXXjwatt Or should we use CocoaFileUtils::RevealFileInFinder(pdfURL);
+ AutoCFRelease<CFStringRef> pdfPath = CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
+ NSString* path = (NSString*)CFStringRef(pdfPath);
+ NSWorkspace* ws = [NSWorkspace sharedWorkspace];
+ [ws openFile:path];
+ break;
+ }
+ case kPMDestinationFile: {
+ AutoCFRelease<CFURLRef> destURL(nullptr);
+ status =
+ ::PMSessionCopyDestinationLocation(mPrintSession, mPMPrintSettings, destURL.receive());
+ if (status == noErr) {
+ AutoCFRelease<CFStringRef> sourcePathRef =
+ CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
+ NSString* sourcePath = (NSString*)CFStringRef(sourcePathRef);
+# ifdef DEBUG
+ AutoCFRelease<CFStringRef> destPathRef =
+ CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
+ NSString* destPath = (NSString*)CFStringRef(destPathRef);
+ NSString* destPathExt = [destPath pathExtension];
+ MOZ_ASSERT([destPathExt isEqualToString:@"pdf"],
+ "nsDeviceContextSpecX::Init only allows '.pdf' for now");
+ // We could use /usr/sbin/cupsfilter to convert the PDF to PS, but
+ // currently we don't.
+# endif
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ if ([fileManager fileExistsAtPath:sourcePath]) {
+ NSURL* src = static_cast<NSURL*>(CFURLRef(pdfURL));
+ NSURL* dest = static_cast<NSURL*>(CFURLRef(destURL));
+ bool ok = [fileManager replaceItemAtURL:dest
+ withItemAtURL:src
+ backupItemName:nil
+ options:NSFileManagerItemReplacementUsingNewMetadataOnly
+ resultingItemURL:nil
+ error:nil];
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("nsDeviceContextSpecX::Init doesn't set "
+ "mPrintViaSkPDF for other values");
+ }
+
+ return (status == noErr) ? NS_OK : NS_ERROR_FAILURE;
+ }
+#endif
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsDeviceContextSpecX::GetPaperRect(double* aTop, double* aLeft, double* aBottom,
+ double* aRight) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ PMRect paperRect;
+ ::PMGetAdjustedPaperRect(mPageFormat, &paperRect);
+
+ *aTop = paperRect.top;
+ *aLeft = paperRect.left;
+ *aBottom = paperRect.bottom;
+ *aRight = paperRect.right;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecX::MakePrintTarget() {
+ double top, left, bottom, right;
+ GetPaperRect(&top, &left, &bottom, &right);
+ const double width = right - left;
+ const double height = bottom - top;
+ IntSize size = IntSize::Ceil(width, height);
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ // TODO: Add support for stream printing via SkPDF if we enable that again.
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mTempFile));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsAutoCString tempPath("tmp-printing.pdf");
+ mTempFile->AppendNative(tempPath);
+ rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ mTempFile->GetNativePath(tempPath);
+ auto stream = MakeUnique<SkFILEWStream>(tempPath.get());
+ return PrintTargetSkPDF::CreateOrNull(std::move(stream), size);
+ }
+#endif
+
+ return PrintTargetCG::CreateOrNull(mOutputStream, mPrintSession, mPageFormat, mPMPrintSettings,
+ size);
+}
diff --git a/widget/cocoa/nsDragService.h b/widget/cocoa/nsDragService.h
new file mode 100644
index 0000000000..da5211fbab
--- /dev/null
+++ b/widget/cocoa/nsDragService.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsDragService_h_
+#define nsDragService_h_
+
+#include "nsBaseDragService.h"
+#include "nsChildView.h"
+
+#include <Cocoa/Cocoa.h>
+
+class nsDragService : public nsBaseDragService {
+ public:
+ nsDragService();
+
+ // nsBaseDragService
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables, const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) override;
+ // nsIDragService
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) override;
+ NS_IMETHOD UpdateDragImage(nsINode* aImage, int32_t aImageX, int32_t aImageY) override;
+
+ // nsIDragSession
+ NS_IMETHOD GetData(nsITransferable* aTransferable, uint32_t aItemIndex) override;
+ NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) override;
+ NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override;
+
+ void DragMovedWithView(NSDraggingSession* aSession, NSPoint aPoint);
+
+ protected:
+ virtual ~nsDragService();
+
+ private:
+ // Creates and returns the drag image for a drag. aImagePoint will be set to
+ // the origin of the drag relative to mNativeDragView.
+ NSImage* ConstructDragImage(nsINode* aDOMNode,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ NSPoint* aImagePoint);
+
+ // Creates and returns the drag image for a drag. aPoint should be the origin
+ // of the drag, for example the mouse coordinate of the mousedown event.
+ // aDragRect will be set the area of the drag relative to this.
+ NSImage* ConstructDragImage(nsINode* aDOMNode,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ mozilla::CSSIntPoint aPoint, mozilla::LayoutDeviceIntRect* aDragRect);
+
+ nsCOMPtr<nsIArray> mDataItems; // only valid for a drag started within gecko
+ ChildView* mNativeDragView;
+ NSEvent* mNativeDragEvent;
+
+ bool mDragImageChanged;
+};
+
+#endif // nsDragService_h_
diff --git a/widget/cocoa/nsDragService.mm b/widget/cocoa/nsDragService.mm
new file mode 100644
index 0000000000..4ac5c3cbc0
--- /dev/null
+++ b/widget/cocoa/nsDragService.mm
@@ -0,0 +1,477 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "mozilla/Logging.h"
+
+#include "gfxContext.h"
+#include "nsArrayUtils.h"
+#include "nsDragService.h"
+#include "nsArrayUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+#include "nsClipboard.h"
+#include "nsXPCOM.h"
+#include "nsCOMPtr.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsLinebreakConverter.h"
+#include "nsINode.h"
+#include "nsRect.h"
+#include "nsPoint.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsIContent.h"
+#include "nsView.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "gfxPlatform.h"
+#include "nsDeviceContext.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+extern mozilla::LazyLogModule sCocoaLog;
+
+extern NSPasteboard* globalDragPboard;
+extern ChildView* gLastDragView;
+extern NSEvent* gLastDragMouseDownEvent;
+extern bool gUserCancelledDrag;
+
+// This global makes the transferable array available to Cocoa's promised
+// file destination callback.
+mozilla::StaticRefPtr<nsIArray> gDraggedTransferables;
+
+nsDragService::nsDragService()
+ : mNativeDragView(nil), mNativeDragEvent(nil), mDragImageChanged(false) {}
+
+nsDragService::~nsDragService() {}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode, const Maybe<CSSIntRegion>& aRegion,
+ NSPoint* aDragPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ LayoutDeviceIntRect dragRect(0, 0, 20, 20);
+ NSImage* image = ConstructDragImage(mSourceNode, aRegion, mScreenPosition, &dragRect);
+ if (!image) {
+ // if no image was returned, just draw a rectangle
+ NSSize size;
+ size.width = nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.width, scaleFactor);
+ size.height = nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.height, scaleFactor);
+ image = [NSImage imageWithSize:size
+ flipped:YES
+ drawingHandler:^BOOL(NSRect dstRect) {
+ [[NSColor grayColor] set];
+ NSBezierPath* path = [NSBezierPath bezierPathWithRect:dstRect];
+ [path setLineWidth:2.0];
+ [path stroke];
+ return YES;
+ }];
+ }
+
+ LayoutDeviceIntPoint pt(dragRect.x, dragRect.YMost());
+ NSPoint point = nsCocoaUtils::DevPixelsToCocoaPoints(pt, scaleFactor);
+ point.y = nsCocoaUtils::FlippedScreenY(point.y);
+
+ point = nsCocoaUtils::ConvertPointFromScreen([mNativeDragView window], point);
+ *aDragPoint = [mNativeDragView convertPoint:point fromView:nil];
+
+ return image;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode, const Maybe<CSSIntRegion>& aRegion,
+ CSSIntPoint aPoint, LayoutDeviceIntRect* aDragRect) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ RefPtr<SourceSurface> surface;
+ nsPresContext* pc;
+ nsresult rv = DrawDrag(aDOMNode, aRegion, aPoint, aDragRect, &surface, &pc);
+ if (pc && (!aDragRect->width || !aDragRect->height)) {
+ // just use some suitable defaults
+ int32_t size = nsCocoaUtils::CocoaPointsToDevPixels(20, scaleFactor);
+ aDragRect->SetRect(pc->CSSPixelsToDevPixels(aPoint.x), pc->CSSPixelsToDevPixels(aPoint.y), size,
+ size);
+ }
+
+ if (NS_FAILED(rv) || !surface) return nil;
+
+ uint32_t width = aDragRect->width;
+ uint32_t height = aDragRect->height;
+
+ RefPtr<DataSourceSurface> dataSurface =
+ Factory::CreateDataSourceSurface(IntSize(width, height), SurfaceFormat::B8G8R8A8);
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map)) {
+ return nil;
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride, dataSurface->GetFormat());
+ if (!dt) {
+ dataSurface->Unmap();
+ return nil;
+ }
+
+ dt->FillRect(gfx::Rect(0, 0, width, height), SurfacePattern(surface, ExtendMode::CLAMP),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+
+ NSBitmapImageRep* imageRep =
+ [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+ pixelsWide:width
+ pixelsHigh:height
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bytesPerRow:width * 4
+ bitsPerPixel:32];
+
+ uint8_t* dest = [imageRep bitmapData];
+ for (uint32_t i = 0; i < height; ++i) {
+ uint8_t* src = map.mData + i * map.mStride;
+ for (uint32_t j = 0; j < width; ++j) {
+ // Reduce transparency overall by multipying by a factor. Remember, Alpha
+ // is premultipled here. Also, Quartz likes RGBA, so do that translation as well.
+#ifdef IS_BIG_ENDIAN
+ dest[0] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+#else
+ dest[0] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+#endif
+ src += 4;
+ dest += 4;
+ }
+ }
+ dataSurface->Unmap();
+
+ NSImage* image =
+ [[NSImage alloc] initWithSize:NSMakeSize(width / scaleFactor, height / scaleFactor)];
+ [image addRepresentation:imageRep];
+ [imageRep release];
+
+ return [image autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+nsresult nsDragService::InvokeDragSessionImpl(nsIArray* aTransferableArray,
+ const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ if (!gLastDragView) {
+ // gLastDragView is non-null between -[ChildView mouseDown:] and -[ChildView mouseUp:].
+ // If we get here with gLastDragView being null, that means that the mouse button has already
+ // been released. In that case we need to abort the drag because the OS won't know where to drop
+ // whatever's being dragged, and we might end up with a stuck drag & drop session.
+ return NS_ERROR_FAILURE;
+ }
+
+ mDataItems = aTransferableArray;
+
+ // Save the transferables away in case a promised file callback is invoked.
+ gDraggedTransferables = aTransferableArray;
+
+ // We need to retain the view and the event during the drag in case either
+ // gets destroyed.
+ mNativeDragView = [gLastDragView retain];
+ mNativeDragEvent = [gLastDragMouseDownEvent retain];
+
+ gUserCancelledDrag = false;
+
+ NSPasteboardItem* pbItem = [NSPasteboardItem new];
+ NSMutableArray* types = [NSMutableArray arrayWithCapacity:5];
+
+ if (gDraggedTransferables) {
+ uint32_t count = 0;
+ gDraggedTransferables->GetLength(&count);
+
+ for (uint32_t j = 0; j < count; j++) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(aTransferableArray, j);
+ if (!currentTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Transform the transferable to an NSDictionary
+ NSDictionary* pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(currentTransferable);
+ if (!pasteboardOutputDict) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write everything out to the general pasteboard
+ [types addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ // Gecko is initiating this drag so we always want its own views to
+ // consider it. Add our wildcard type to the pasteboard to accomplish
+ // this.
+ [types addObject:[UTIHelper stringFromPboardType:kMozWildcardPboardType]];
+ }
+ }
+ [pbItem setDataProvider:mNativeDragView forTypes:types];
+
+ NSPoint draggingPoint;
+ NSImage* image = ConstructDragImage(mSourceNode, aRegion, &draggingPoint);
+
+ NSRect localDragRect = image.alignmentRect;
+ localDragRect.origin.x = draggingPoint.x;
+ localDragRect.origin.y = draggingPoint.y - localDragRect.size.height;
+
+ NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pbItem];
+ [pbItem release];
+ [dragItem setDraggingFrame:localDragRect contents:image];
+
+ nsBaseDragService::StartDragSession();
+ nsBaseDragService::OpenDragPopup();
+
+ NSDraggingSession* draggingSession = [mNativeDragView
+ beginDraggingSessionWithItems:[NSArray arrayWithObject:[dragItem autorelease]]
+ event:mNativeDragEvent
+ source:mNativeDragView];
+ draggingSession.animatesToStartingPositionsOnCancelOrFail =
+ !mDataTransfer || mDataTransfer->MozShowFailAnimation();
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get flavor list that includes all acceptable flavors (including ones obtained through
+ // conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if this drag originated within Mozilla we should just use the cached data from
+ // when the drag started if possible
+ if (mDataItems) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(mDataItems, aItemIndex);
+ if (currentTransferable) {
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ nsCOMPtr<nsISupports> dataSupports;
+ rv = currentTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(dataSupports));
+ if (NS_SUCCEEDED(rv)) {
+ aTransferable->SetTransferData(flavorStr.get(), dataSupports);
+ return NS_OK; // maybe try to fill in more types? Is there a point?
+ }
+ }
+ }
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (!droppedItems) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t itemCount = [droppedItems count];
+ if (aItemIndex >= itemCount) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPasteboardItem* item = [droppedItems objectAtIndex:aItemIndex];
+ if (!item) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // now check the actual clipboard for data
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable, flavors[i], item);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *_retval = false;
+
+ if (!globalDragPboard) return NS_ERROR_FAILURE;
+
+ nsDependentCString dataFlavor(aDataFlavor);
+
+ // first see if we have data for this in our cached transferable
+ if (mDataItems) {
+ uint32_t dataItemsCount;
+ mDataItems->GetLength(&dataItemsCount);
+ for (unsigned int i = 0; i < dataItemsCount; i++) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(mDataItems, i);
+ if (!currentTransferable) continue;
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = currentTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) continue;
+
+ for (uint32_t j = 0; j < flavors.Length(); j++) {
+ if (dataFlavor.Equals(flavors[j])) {
+ *_retval = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ const NSString* type = nil;
+ bool allowFileURL = false;
+ if (dataFlavor.EqualsLiteral(kFileMime)) {
+ type = [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
+ allowFileURL = true;
+ } else if (dataFlavor.EqualsLiteral(kTextMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ } else if (dataFlavor.EqualsLiteral(kHTMLMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ } else if (dataFlavor.EqualsLiteral(kURLMime) || dataFlavor.EqualsLiteral(kURLDataMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlPboardType];
+ } else if (dataFlavor.EqualsLiteral(kURLDescriptionMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
+ } else if (dataFlavor.EqualsLiteral(kRTFMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
+ } else if (dataFlavor.EqualsLiteral(kCustomTypesMime)) {
+ type = [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
+ }
+
+ NSString* availableType =
+ [globalDragPboard availableTypeFromArray:[NSArray arrayWithObjects:(id)type, nil]];
+ if (availableType && nsCocoaUtils::IsValidPasteboardType(availableType, allowFileURL)) {
+ *_retval = true;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aNumItems = 0;
+
+ // first check to see if we have a number of items cached
+ if (mDataItems) {
+ mDataItems->GetLength(aNumItems);
+ return NS_OK;
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (droppedItems) {
+ *aNumItems = [droppedItems count];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX, int32_t aImageY) {
+ nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY);
+ mDragImageChanged = true;
+ return NS_OK;
+}
+
+void nsDragService::DragMovedWithView(NSDraggingSession* aSession, NSPoint aPoint) {
+ aPoint.y = nsCocoaUtils::FlippedScreenY(aPoint.y);
+
+ // XXX It feels like we should be using the backing scale factor at aPoint
+ // rather than the initial drag view, but I've seen no ill effects of this.
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+ LayoutDeviceIntPoint devPoint = nsCocoaUtils::CocoaPointsToDevPixels(aPoint, scaleFactor);
+
+ // If the image has changed, call enumerateDraggingItemsWithOptions to get
+ // the item being dragged and update its image.
+ if (mDragImageChanged && mNativeDragView) {
+ mDragImageChanged = false;
+
+ nsPresContext* pc = nullptr;
+ nsCOMPtr<nsIContent> content = do_QueryInterface(mImage);
+ if (content) {
+ pc = content->OwnerDoc()->GetPresContext();
+ }
+
+ if (pc) {
+ void (^changeImageBlock)(NSDraggingItem*, NSInteger, BOOL*) =
+ ^(NSDraggingItem* draggingItem, NSInteger idx, BOOL* stop) {
+ // We never add more than one item right now, but check just in case.
+ if (idx > 0) {
+ return;
+ }
+
+ nsPoint pt =
+ LayoutDevicePixel::ToAppUnits(devPoint, pc->DeviceContext()->AppUnitsPerDevPixel());
+ CSSIntPoint screenPoint = CSSIntPoint(nsPresContext::AppUnitsToIntCSSPixels(pt.x),
+ nsPresContext::AppUnitsToIntCSSPixels(pt.y));
+
+ // Create a new image; if one isn't returned don't change the current one.
+ LayoutDeviceIntRect newRect;
+ NSImage* image = ConstructDragImage(mSourceNode, Nothing(), screenPoint, &newRect);
+ if (image) {
+ NSRect draggingRect = nsCocoaUtils::GeckoRectToCocoaRectDevPix(newRect, scaleFactor);
+ [draggingItem setDraggingFrame:draggingRect contents:image];
+ }
+ };
+
+ [aSession enumerateDraggingItemsWithOptions:NSDraggingItemEnumerationConcurrent
+ forView:nil
+ classes:[NSArray arrayWithObject:[NSPasteboardItem class]]
+ searchOptions:@{}
+ usingBlock:changeImageBlock];
+ }
+ }
+
+ DragMoved(devPoint.x, devPoint.y);
+}
+
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mNativeDragView) {
+ [mNativeDragView release];
+ mNativeDragView = nil;
+ }
+ if (mNativeDragEvent) {
+ [mNativeDragEvent release];
+ mNativeDragEvent = nil;
+ }
+
+ mUserCancelled = gUserCancelledDrag;
+
+ nsresult rv = nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+ mDataItems = nullptr;
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsFilePicker.h b/widget/cocoa/nsFilePicker.h
new file mode 100644
index 0000000000..cdc17f8646
--- /dev/null
+++ b/widget/cocoa/nsFilePicker.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsFilePicker_h_
+#define nsFilePicker_h_
+
+#include "nsBaseFilePicker.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsCOMArray.h"
+#include "nsTArray.h"
+
+class nsILocalFileMac;
+@class NSArray;
+
+class nsFilePicker : public nsBaseFilePicker {
+ public:
+ nsFilePicker();
+ using nsIFilePicker::ResultCode;
+
+ NS_DECL_ISUPPORTS
+
+ // nsIFilePicker (less what's in nsBaseFilePicker)
+ NS_IMETHOD GetDefaultString(nsAString& aDefaultString) override;
+ NS_IMETHOD SetDefaultString(const nsAString& aDefaultString) override;
+ NS_IMETHOD GetDefaultExtension(nsAString& aDefaultExtension) override;
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD SetDefaultExtension(const nsAString& aDefaultExtension) override;
+ NS_IMETHOD GetFile(nsIFile** aFile) override;
+ NS_IMETHOD GetFileURL(nsIURI** aFileURL) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+ NS_IMETHOD AppendFilter(const nsAString& aTitle, const nsAString& aFilter) override;
+
+ /**
+ * Returns the current filter list in the format used by Cocoa's NSSavePanel
+ * and NSOpenPanel.
+ * Returns nil if no filter currently apply.
+ */
+ NSArray* GetFilterList();
+
+ protected:
+ virtual ~nsFilePicker();
+
+ virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override;
+ nsresult Show(ResultCode* _retval) override;
+
+ // actual implementations of get/put dialogs using NSOpenPanel & NSSavePanel
+ // aFile is an existing but unspecified file. These functions must specify it.
+ //
+ // will return |returnCancel| or |returnOK| as result.
+ ResultCode GetLocalFiles(bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles);
+ ResultCode GetLocalFolder(nsIFile** outFile);
+ ResultCode PutLocalFile(nsIFile** outFile);
+
+ void SetDialogTitle(const nsString& inTitle, id aDialog);
+ NSString* PanelDefaultDirectory();
+ NSView* GetAccessoryView();
+
+ nsString mTitle;
+ nsCOMArray<nsIFile> mFiles;
+ nsString mDefaultFilename;
+
+ nsTArray<nsString> mFilters;
+ nsTArray<nsString> mTitles;
+
+ int32_t mSelectedTypeIndex;
+};
+
+#endif // nsFilePicker_h_
diff --git a/widget/cocoa/nsFilePicker.mm b/widget/cocoa/nsFilePicker.mm
new file mode 100644
index 0000000000..ec0100e569
--- /dev/null
+++ b/widget/cocoa/nsFilePicker.mm
@@ -0,0 +1,639 @@
+/* -*- Mode: C++; tab-width: 2; 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 <Cocoa/Cocoa.h>
+
+#include "nsFilePicker.h"
+#include "nsCOMPtr.h"
+#include "nsReadableUtils.h"
+#include "nsNetUtil.h"
+#include "nsIFile.h"
+#include "nsILocalFileMac.h"
+#include "nsArrayEnumerator.h"
+#include "nsIStringBundle.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Preferences.h"
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+const float kAccessoryViewPadding = 5;
+const int kSaveTypeControlTag = 1;
+
+static bool gCallSecretHiddenFileAPI = false;
+const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles";
+
+/**
+ * This class is an observer of NSPopUpButton selection change.
+ */
+@interface NSPopUpButtonObserver : NSObject {
+ NSPopUpButton* mPopUpButton;
+ NSOpenPanel* mOpenPanel;
+ nsFilePicker* mFilePicker;
+}
+- (void)setPopUpButton:(NSPopUpButton*)aPopUpButton;
+- (void)setOpenPanel:(NSOpenPanel*)aOpenPanel;
+- (void)setFilePicker:(nsFilePicker*)aFilePicker;
+- (void)menuChangedItem:(NSNotification*)aSender;
+@end
+
+NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
+
+// We never want to call the secret show hidden files API unless the pref
+// has been set. Once the pref has been set we always need to call it even
+// if it disappears so that we stop showing hidden files if a user deletes
+// the pref. If the secret API was used once and things worked out it should
+// continue working for subsequent calls so the user is at no more risk.
+static void SetShowHiddenFileState(NSSavePanel* panel) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ bool show = false;
+ if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) {
+ gCallSecretHiddenFileAPI = true;
+ }
+
+ if (gCallSecretHiddenFileAPI) {
+ // invoke a method to get a Cocoa-internal nav view
+ SEL navViewSelector = @selector(_navView);
+ NSMethodSignature* navViewSignature = [panel methodSignatureForSelector:navViewSelector];
+ if (!navViewSignature) return;
+ NSInvocation* navViewInvocation = [NSInvocation invocationWithMethodSignature:navViewSignature];
+ [navViewInvocation setSelector:navViewSelector];
+ [navViewInvocation setTarget:panel];
+ [navViewInvocation invoke];
+
+ // get the returned nav view
+ id navView = nil;
+ [navViewInvocation getReturnValue:&navView];
+
+ // invoke the secret show hidden file state method on the nav view
+ SEL showHiddenFilesSelector = @selector(setShowsHiddenFiles:);
+ NSMethodSignature* showHiddenFilesSignature =
+ [navView methodSignatureForSelector:showHiddenFilesSelector];
+ if (!showHiddenFilesSignature) return;
+ NSInvocation* showHiddenFilesInvocation =
+ [NSInvocation invocationWithMethodSignature:showHiddenFilesSignature];
+ [showHiddenFilesInvocation setSelector:showHiddenFilesSelector];
+ [showHiddenFilesInvocation setTarget:navView];
+ [showHiddenFilesInvocation setArgument:&show atIndex:2];
+ [showHiddenFilesInvocation invoke];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsFilePicker::nsFilePicker() : mSelectedTypeIndex(0) {}
+
+nsFilePicker::~nsFilePicker() {}
+
+void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { mTitle = aTitle; }
+
+NSView* nsFilePicker::GetAccessoryView() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSView* accessoryView = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] autorelease];
+
+ // Set a label's default value.
+ NSString* label = @"Format:";
+
+ // Try to get the localized string.
+ nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv =
+ sbs->CreateBundle("chrome://global/locale/filepicker.properties", getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString locaLabel;
+ rv = bundle->GetStringFromName("formatLabel", locaLabel);
+ if (NS_SUCCEEDED(rv)) {
+ label = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(locaLabel.get())
+ length:locaLabel.Length()];
+ }
+ }
+
+ // set up label text field
+ NSTextField* textField = [[[NSTextField alloc] init] autorelease];
+ [textField setEditable:NO];
+ [textField setSelectable:NO];
+ [textField setDrawsBackground:NO];
+ [textField setBezeled:NO];
+ [textField setBordered:NO];
+ [textField setFont:[NSFont labelFontOfSize:13.0]];
+ [textField setStringValue:label];
+ [textField setTag:0];
+ [textField sizeToFit];
+
+ // set up popup button
+ NSPopUpButton* popupButton = [[[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)
+ pullsDown:NO] autorelease];
+ uint32_t numMenuItems = mTitles.Length();
+ for (uint32_t i = 0; i < numMenuItems; i++) {
+ const nsString& currentTitle = mTitles[i];
+ NSString* titleString;
+ if (currentTitle.IsEmpty()) {
+ const nsString& currentFilter = mFilters[i];
+ titleString =
+ [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(currentFilter.get())
+ length:currentFilter.Length()];
+ } else {
+ titleString =
+ [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(currentTitle.get())
+ length:currentTitle.Length()];
+ }
+ [popupButton addItemWithTitle:titleString];
+ [titleString release];
+ }
+ if (mSelectedTypeIndex >= 0 && (uint32_t)mSelectedTypeIndex < numMenuItems)
+ [popupButton selectItemAtIndex:mSelectedTypeIndex];
+ [popupButton setTag:kSaveTypeControlTag];
+ [popupButton sizeToFit]; // we have to do sizeToFit to get the height calculated for us
+ // This is just a default width that works well, doesn't truncate the vast majority of
+ // things that might end up in the menu.
+ [popupButton setFrameSize:NSMakeSize(180, [popupButton frame].size.height)];
+
+ // position everything based on control sizes with kAccessoryViewPadding pix padding
+ // on each side kAccessoryViewPadding pix horizontal padding between controls
+ float greatestHeight = [textField frame].size.height;
+ if ([popupButton frame].size.height > greatestHeight)
+ greatestHeight = [popupButton frame].size.height;
+ float totalViewHeight = greatestHeight + kAccessoryViewPadding * 2;
+ float totalViewWidth =
+ [textField frame].size.width + [popupButton frame].size.width + kAccessoryViewPadding * 3;
+ [accessoryView setFrameSize:NSMakeSize(totalViewWidth, totalViewHeight)];
+
+ float textFieldOriginY =
+ ((greatestHeight - [textField frame].size.height) / 2 + 1) + kAccessoryViewPadding;
+ [textField setFrameOrigin:NSMakePoint(kAccessoryViewPadding, textFieldOriginY)];
+
+ float popupOriginX = [textField frame].size.width + kAccessoryViewPadding * 2;
+ float popupOriginY =
+ ((greatestHeight - [popupButton frame].size.height) / 2) + kAccessoryViewPadding;
+ [popupButton setFrameOrigin:NSMakePoint(popupOriginX, popupOriginY)];
+
+ [accessoryView addSubview:textField];
+ [accessoryView addSubview:popupButton];
+ return accessoryView;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Display the file dialog
+nsresult nsFilePicker::Show(ResultCode* retval) {
+ NS_ENSURE_ARG_POINTER(retval);
+
+ *retval = returnCancel;
+
+ ResultCode userClicksOK = returnCancel;
+
+ mFiles.Clear();
+ nsCOMPtr<nsIFile> theFile;
+
+ // Note that GetLocalFolder shares a lot of code with GetLocalFiles.
+ // Could combine the functions and just pass the mode in.
+ switch (mMode) {
+ case modeOpen:
+ userClicksOK = GetLocalFiles(false, mFiles);
+ break;
+
+ case modeOpenMultiple:
+ userClicksOK = GetLocalFiles(true, mFiles);
+ break;
+
+ case modeSave:
+ userClicksOK = PutLocalFile(getter_AddRefs(theFile));
+ break;
+
+ case modeGetFolder:
+ userClicksOK = GetLocalFolder(getter_AddRefs(theFile));
+ break;
+
+ default:
+ NS_ERROR("Unknown file picker mode");
+ break;
+ }
+
+ if (theFile) mFiles.AppendObject(theFile);
+
+ *retval = userClicksOK;
+ return NS_OK;
+}
+
+static void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters) {
+ // If we show all file types, also "expose" bundles' contents.
+ [aPanel setTreatsFilePackagesAsDirectories:!aFilters];
+
+ [aPanel setAllowedFileTypes:aFilters];
+}
+
+@implementation NSPopUpButtonObserver
+- (void)setPopUpButton:(NSPopUpButton*)aPopUpButton {
+ mPopUpButton = aPopUpButton;
+}
+
+- (void)setOpenPanel:(NSOpenPanel*)aOpenPanel {
+ mOpenPanel = aOpenPanel;
+}
+
+- (void)setFilePicker:(nsFilePicker*)aFilePicker {
+ mFilePicker = aFilePicker;
+}
+
+- (void)menuChangedItem:(NSNotification*)aSender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ int32_t selectedItem = [mPopUpButton indexOfSelectedItem];
+ if (selectedItem < 0) {
+ return;
+ }
+
+ mFilePicker->SetFilterIndex(selectedItem);
+ UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList());
+
+ NS_OBJC_END_TRY_BLOCK_RETURN();
+}
+@end
+
+// Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in the dialog.
+nsIFilePicker::ResultCode nsFilePicker::GetLocalFiles(bool inAllowMultiple,
+ nsCOMArray<nsIFile>& outFiles) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSOpenPanel* thePanel = [NSOpenPanel openPanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ // Set the options for how the get file dialog will appear
+ SetDialogTitle(mTitle, thePanel);
+ [thePanel setAllowsMultipleSelection:inAllowMultiple];
+ [thePanel setCanSelectHiddenExtension:YES];
+ [thePanel setCanChooseDirectories:NO];
+ [thePanel setCanChooseFiles:YES];
+ [thePanel setResolvesAliases:YES];
+
+ // Get filters
+ // filters may be null, if we should allow all file types.
+ NSArray* filters = GetFilterList();
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+
+ // if this is the "Choose application..." dialog, and no other start
+ // dir has been set, then use the Applications folder.
+ if (!theDir) {
+ if (filters && [filters count] == 1 &&
+ [(NSString*)[filters objectAtIndex:0] isEqualToString:@"app"])
+ theDir = @"/Applications/";
+ else
+ theDir = @"";
+ }
+
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+
+ int result;
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ if (mFilters.Length() > 1) {
+ // [NSURL initWithString:] (below) throws an exception if URLString is nil.
+
+ NSPopUpButtonObserver* observer = [[NSPopUpButtonObserver alloc] init];
+
+ NSView* accessoryView = GetAccessoryView();
+ [thePanel setAccessoryView:accessoryView];
+
+ [observer setPopUpButton:[accessoryView viewWithTag:kSaveTypeControlTag]];
+ [observer setOpenPanel:thePanel];
+ [observer setFilePicker:this];
+
+ [[NSNotificationCenter defaultCenter] addObserver:observer
+ selector:@selector(menuChangedItem:)
+ name:NSMenuWillSendActionNotification
+ object:nil];
+
+ UpdatePanelFileTypes(thePanel, filters);
+ result = [thePanel runModal];
+
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ [observer release];
+ } else {
+ // If we show all file types, also "expose" bundles' contents.
+ if (!filters) {
+ [thePanel setTreatsFilePackagesAsDirectories:YES];
+ }
+ [thePanel setAllowedFileTypes:filters];
+ result = [thePanel runModal];
+ }
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (result == NSFileHandlingPanelCancelButton) return retVal;
+
+ // Converts data from a NSArray of NSURL to the returned format.
+ // We should be careful to not call [thePanel URLs] more than once given that
+ // it creates a new array each time.
+ // We are using Fast Enumeration, thus the NSURL array is created once then
+ // iterated.
+ for (NSURL* url in [thePanel URLs]) {
+ if (!url) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)url))) {
+ outFiles.AppendObject(localFile);
+ }
+ }
+
+ if (outFiles.Count() > 0) retVal = returnOK;
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
+}
+
+// Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in the dialog.
+nsIFilePicker::ResultCode nsFilePicker::GetLocalFolder(nsIFile** outFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer");
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSOpenPanel* thePanel = [NSOpenPanel openPanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ // Set the options for how the get file dialog will appear
+ SetDialogTitle(mTitle, thePanel);
+ [thePanel setAllowsMultipleSelection:NO];
+ [thePanel setCanSelectHiddenExtension:YES];
+ [thePanel setCanChooseDirectories:YES];
+ [thePanel setCanChooseFiles:NO];
+ [thePanel setResolvesAliases:YES];
+ [thePanel setCanCreateDirectories:YES];
+
+ // packages != folders
+ [thePanel setTreatsFilePackagesAsDirectories:NO];
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int result = [thePanel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (result == NSFileHandlingPanelCancelButton) return retVal;
+
+ // get the path for the folder (we allow just 1, so that's all we get)
+ NSURL* theURL = [[thePanel URLs] objectAtIndex:0];
+ if (theURL) {
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)theURL))) {
+ *outFile = localFile;
+ NS_ADDREF(*outFile);
+ retVal = returnOK;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
+}
+
+// Returns |returnOK| if the user presses OK in the dialog.
+nsIFilePicker::ResultCode nsFilePicker::PutLocalFile(nsIFile** outFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer");
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSSavePanel* thePanel = [NSSavePanel savePanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ SetDialogTitle(mTitle, thePanel);
+
+ // set up accessory view for file format options
+ NSView* accessoryView = GetAccessoryView();
+ [thePanel setAccessoryView:accessoryView];
+
+ // set up default file name
+ NSString* defaultFilename = [NSString stringWithCharacters:(const unichar*)mDefaultFilename.get()
+ length:mDefaultFilename.Length()];
+
+ // Set up the allowed type. This prevents the extension from being selected.
+ NSString* extension = defaultFilename.pathExtension;
+ if (extension.length != 0) {
+ thePanel.allowedFileTypes = @[ extension ];
+ }
+ // Allow users to change the extension.
+ thePanel.allowsOtherFileTypes = YES;
+
+ // If extensions are hidden and we’re saving a file with multiple extensions,
+ // only the last extension will be hidden in the panel (".tar.gz" will become
+ // ".tar"). If the remaining extension is known, the OS will think that we're
+ // trying to add a non-default extension. To avoid the confusion, we ensure
+ // that all extensions are shown in the panel if the remaining extension is
+ // known by the OS.
+ NSString* fileName = [[defaultFilename lastPathComponent] stringByDeletingPathExtension];
+ NSString* otherExtension = fileName.pathExtension;
+ if (otherExtension.length != 0) {
+ // There's another extension here. Get the UTI.
+ CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
+ (CFStringRef)otherExtension, NULL);
+ if (type) {
+ if (!CFStringHasPrefix(type, CFSTR("dyn."))) {
+ // We have a UTI, otherwise the type would have a "dyn." prefix. Ensure
+ // extensions are shown in the panel.
+ [thePanel setExtensionHidden:NO];
+ }
+ CFRelease(type);
+ }
+ }
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+
+ // load the panel
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ [thePanel setNameFieldStringValue:defaultFilename];
+ int result = [thePanel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+ if (result == NSFileHandlingPanelCancelButton) return retVal;
+
+ // get the save type
+ NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag];
+ if (popupButton) {
+ mSelectedTypeIndex = [popupButton indexOfSelectedItem];
+ }
+
+ NSURL* fileURL = [thePanel URL];
+ if (fileURL) {
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)fileURL))) {
+ *outFile = localFile;
+ NS_ADDREF(*outFile);
+ // We tell if we are replacing or not by just looking to see if the file exists.
+ // The user could not have hit OK and not meant to replace the file.
+ if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]])
+ retVal = returnReplace;
+ else
+ retVal = returnOK;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnCancel);
+}
+
+NSArray* nsFilePicker::GetFilterList() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mFilters.Length()) {
+ return nil;
+ }
+
+ if (mFilters.Length() <= (uint32_t)mSelectedTypeIndex) {
+ NS_WARNING("An out of range index has been selected. Using the first index instead.");
+ mSelectedTypeIndex = 0;
+ }
+
+ const nsString& filterWide = mFilters[mSelectedTypeIndex];
+ if (!filterWide.Length()) {
+ return nil;
+ }
+
+ if (filterWide.Equals(u"*"_ns)) {
+ return nil;
+ }
+
+ // The extensions in filterWide are in the format "*.ext" but are expected
+ // in the format "ext" by NSOpenPanel. So we need to filter some characters.
+ NSMutableString* filterString = [[[NSMutableString alloc]
+ initWithString:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(filterWide.get())
+ length:filterWide.Length()]] autorelease];
+ NSCharacterSet* set = [NSCharacterSet characterSetWithCharactersInString:@". *"];
+ NSRange range = [filterString rangeOfCharacterFromSet:set];
+ while (range.length) {
+ [filterString replaceCharactersInRange:range withString:@""];
+ range = [filterString rangeOfCharacterFromSet:set];
+ }
+
+ return
+ [[[NSArray alloc] initWithArray:[filterString componentsSeparatedByString:@";"]] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Sets the dialog title to whatever it should be. If it fails, eh,
+// the OS will provide a sensible default.
+void nsFilePicker::SetDialogTitle(const nsString& inTitle, id aPanel) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [aPanel setTitle:[NSString stringWithCharacters:(const unichar*)inTitle.get()
+ length:inTitle.Length()]];
+
+ if (!mOkButtonLabel.IsEmpty()) {
+ [aPanel setPrompt:[NSString stringWithCharacters:(const unichar*)mOkButtonLabel.get()
+ length:mOkButtonLabel.Length()]];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Converts path from an nsIFile into a NSString path
+// If it fails, returns an empty string.
+NSString* nsFilePicker::PanelDefaultDirectory() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* directory = nil;
+ if (mDisplayDirectory) {
+ nsAutoString pathStr;
+ mDisplayDirectory->GetPath(pathStr);
+ directory =
+ [[[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(pathStr.get())
+ length:pathStr.Length()] autorelease];
+ }
+ return directory;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NS_IMETHODIMP nsFilePicker::GetFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ *aFile = nullptr;
+
+ // just return the first file
+ if (mFiles.Count() > 0) {
+ *aFile = mFiles.ObjectAt(0);
+ NS_IF_ADDREF(*aFile);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI** aFileURL) {
+ NS_ENSURE_ARG_POINTER(aFileURL);
+ *aFileURL = nullptr;
+
+ if (mFiles.Count() == 0) return NS_OK;
+
+ return NS_NewFileURI(aFileURL, mFiles.ObjectAt(0));
+}
+
+NS_IMETHODIMP nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
+ return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
+}
+
+NS_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString) {
+ mDefaultFilename = aString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::GetDefaultString(nsAString& aString) { return NS_ERROR_FAILURE; }
+
+// The default extension to use for files
+NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
+ aExtension.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension) { return NS_OK; }
+
+// Append an entry to the filters array
+NS_IMETHODIMP
+nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
+ // "..apps" has to be translated with native executable extensions.
+ if (aFilter.EqualsLiteral("..apps")) {
+ mFilters.AppendElement(u"*.app"_ns);
+ } else {
+ mFilters.AppendElement(aFilter);
+ }
+ mTitles.AppendElement(aTitle);
+
+ return NS_OK;
+}
+
+// Get the filter index - do we still need this?
+NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
+ *aFilterIndex = mSelectedTypeIndex;
+ return NS_OK;
+}
+
+// Set the filter index - do we still need this?
+NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
+ mSelectedTypeIndex = aFilterIndex;
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsLookAndFeel.h b/widget/cocoa/nsLookAndFeel.h
new file mode 100644
index 0000000000..adce685a4e
--- /dev/null
+++ b/widget/cocoa/nsLookAndFeel.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsLookAndFeel_h_
+#define nsLookAndFeel_h_
+#include "nsXPLookAndFeel.h"
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aColor) override;
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+
+ virtual char16_t GetPasswordCharacterImpl() override {
+ // unicode value for the bullet character, used for password textfields.
+ return 0x2022;
+ }
+
+ void RecordLookAndFeelSpecificTelemetry() override {
+ RecordAccessibilityTelemetry();
+ }
+
+ // Having a separate, static method allows us to rely on the same
+ // chunk of telemetry logging code at initialization and when we
+ // recieve an event that changes the value of our telemetry probe.
+ static void RecordAccessibilityTelemetry();
+
+ protected:
+ static bool SystemWantsDarkTheme();
+ static bool IsSystemOrientationRTL();
+ static nscolor ProcessSelectionBackground(nscolor aColor,
+ ColorScheme aScheme);
+};
+
+#endif // nsLookAndFeel_h_
diff --git a/widget/cocoa/nsLookAndFeel.mm b/widget/cocoa/nsLookAndFeel.mm
new file mode 100644
index 0000000000..65e852b38e
--- /dev/null
+++ b/widget/cocoa/nsLookAndFeel.mm
@@ -0,0 +1,691 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#include "AppearanceOverride.h"
+#include "mozilla/widget/ThemeChangeKind.h"
+#include "nsLookAndFeel.h"
+#include "nsCocoaFeatures.h"
+#include "nsNativeThemeColors.h"
+#include "nsStyleConsts.h"
+#include "nsCocoaFeatures.h"
+#include "nsIContent.h"
+#include "gfxFont.h"
+#include "gfxFontConstants.h"
+#include "gfxPlatformMac.h"
+#include "nsCSSColorUtils.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/widget/WidgetMessageUtils.h"
+#include "SDKDeclarations.h"
+
+#import <Cocoa/Cocoa.h>
+#import <AppKit/NSColor.h>
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+@interface MOZLookAndFeelDynamicChangeObserver : NSObject
++ (void)startObserving;
+@end
+
+nsLookAndFeel::nsLookAndFeel() = default;
+
+nsLookAndFeel::~nsLookAndFeel() = default;
+
+void nsLookAndFeel::NativeInit() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK
+
+ [MOZLookAndFeelDynamicChangeObserver startObserving];
+ RecordTelemetry();
+
+ NS_OBJC_END_TRY_ABORT_BLOCK
+}
+
+static nscolor GetColorFromNSColor(NSColor* aColor) {
+ NSColor* deviceColor = [aColor colorUsingColorSpaceName:NSDeviceRGBColorSpace];
+ return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
+ (unsigned int)(deviceColor.greenComponent * 255.0),
+ (unsigned int)(deviceColor.blueComponent * 255.0),
+ (unsigned int)(deviceColor.alphaComponent * 255.0));
+}
+
+static nscolor GetColorFromNSColorWithCustomAlpha(NSColor* aColor, float alpha) {
+ NSColor* deviceColor = [aColor colorUsingColorSpaceName:NSDeviceRGBColorSpace];
+ return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
+ (unsigned int)(deviceColor.greenComponent * 255.0),
+ (unsigned int)(deviceColor.blueComponent * 255.0), (unsigned int)(alpha * 255.0));
+}
+
+// Turns an opaque selection color into a partially transparent selection color,
+// which usually leads to better contrast with the text color and which should
+// look more visually appealing in most contexts.
+// The idea is that the text and its regular, non-selected background are
+// usually chosen in such a way that they contrast well. Making the selection
+// color partially transparent causes the selection color to mix with the text's
+// regular background, so the end result will often have better contrast with
+// the text than an arbitrary opaque selection color.
+// The motivating example for this is the light selection color on dark web
+// pages: White text on a light blue selection color has very bad contrast,
+// whereas white text on dark blue (which what you get if you mix
+// partially-transparent light blue with the black textbox background) has much
+// better contrast.
+nscolor nsLookAndFeel::ProcessSelectionBackground(nscolor aColor, ColorScheme aScheme) {
+ if (aScheme == ColorScheme::Dark) {
+ // When we use a dark selection color, we do not change alpha because we do
+ // not use dark selection in content. The dark system color is appropriate for
+ // Firefox UI without needing to adjust its alpha.
+ return aColor;
+ }
+ uint16_t hue, sat, value;
+ uint8_t alpha;
+ nscolor resultColor = aColor;
+ NS_RGB2HSV(resultColor, hue, sat, value, alpha);
+ int factor = 2;
+ alpha = alpha / factor;
+ if (sat > 0) {
+ // The color is not a shade of grey, restore the saturation taken away by
+ // the transparency.
+ sat = mozilla::clamped(sat * factor, 0, 255);
+ } else {
+ // The color is a shade of grey, find the value that looks equivalent
+ // on a white background with the given opacity.
+ value = mozilla::clamped(255 - (255 - value) * factor, 0, 255);
+ }
+ NS_HSV2RGB(resultColor, hue, sat, value, alpha);
+ return resultColor;
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme, nscolor& aColor) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK
+
+ if (@available(macOS 10.14, *)) {
+ // No-op. macOS 10.14+ supports dark mode, so currentAppearance can be set
+ // to either Light or Dark.
+ } else {
+ // System colors before 10.14 are always Light.
+ aScheme = ColorScheme::Light;
+ }
+
+ NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
+
+ nscolor color = 0;
+ switch (aID) {
+ case ColorID::Infobackground:
+ color = aScheme == ColorScheme::Light ? NS_RGB(0xdd, 0xdd, 0xdd)
+ : GetColorFromNSColor(NSColor.windowBackgroundColor);
+ break;
+ case ColorID::Highlight:
+ color = ProcessSelectionBackground(GetColorFromNSColor(NSColor.selectedTextBackgroundColor),
+ aScheme);
+ break;
+ // This is used to gray out the selection when it's not focused. Used with
+ // nsISelectionController::SELECTION_DISABLED.
+ case ColorID::TextSelectDisabledBackground:
+ color = ProcessSelectionBackground(GetColorFromNSColor(NSColor.secondarySelectedControlColor),
+ aScheme);
+ break;
+ case ColorID::MozMenuhoverdisabled:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::MozMenuhover:
+ case ColorID::Selecteditem:
+ color = GetColorFromNSColor(NSColor.alternateSelectedControlColor);
+ break;
+ case ColorID::Accentcolortext:
+ case ColorID::MozMenuhovertext:
+ case ColorID::Selecteditemtext:
+ color = GetColorFromNSColor(NSColor.alternateSelectedControlTextColor);
+ break;
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ color = NS_TRANSPARENT;
+ break;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ case ColorID::Highlighttext:
+ color = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ color = NS_40PERCENT_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ color = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+
+ //
+ // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ //
+ // It's really hard to effectively map these to the Appearance Manager properly,
+ // since they are modeled word for word after the win32 system colors and don't have any
+ // real counterparts in the Mac world. I'm sure we'll be tweaking these for
+ // years to come.
+ //
+ // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that form the defaults
+ // if querying the Appearance Manager fails ;)
+ //
+ case ColorID::MozMacDefaultbuttontext:
+ color = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::MozButtonactivetext:
+ // Pre-macOS 12, pressed buttons were filled with the highlight color and the text was white.
+ // Starting with macOS 12, pressed (non-default) buttons are filled with medium gray and the
+ // text color is the same as in the non-pressed state.
+ color = nsCocoaFeatures::OnMontereyOrLater() ? GetColorFromNSColor(NSColor.controlTextColor)
+ : NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Captiontext:
+ case ColorID::Menutext:
+ case ColorID::Infotext:
+ case ColorID::MozMenubartext:
+ color = GetColorFromNSColor(NSColor.textColor);
+ break;
+ case ColorID::Windowtext:
+ color = GetColorFromNSColor(NSColor.windowFrameTextColor);
+ break;
+ case ColorID::Activecaption:
+ color = GetColorFromNSColor(NSColor.gridColor);
+ break;
+ case ColorID::Activeborder:
+ color = GetColorFromNSColor(NSColor.keyboardFocusIndicatorColor);
+ break;
+ case ColorID::Appworkspace:
+ color = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Background:
+ color = NS_RGB(0x63, 0x63, 0xCE);
+ break;
+ case ColorID::Buttonface:
+ case ColorID::MozButtonhoverface:
+ case ColorID::MozButtonactiveface:
+ case ColorID::MozButtondisabledface:
+ color = GetColorFromNSColor(NSColor.controlColor);
+ if (!NS_GET_A(color)) {
+ color = GetColorFromNSColor(NSColor.controlBackgroundColor);
+ }
+ break;
+ case ColorID::Buttonhighlight:
+ color = GetColorFromNSColor(NSColor.selectedControlColor);
+ break;
+ case ColorID::Inactivecaptiontext:
+ color = NS_RGB(0x45, 0x45, 0x45);
+ break;
+ case ColorID::Scrollbar:
+ color = GetColorFromNSColor(NSColor.scrollBarColor);
+ break;
+ case ColorID::Threedhighlight:
+ color = GetColorFromNSColor(NSColor.highlightColor);
+ break;
+ case ColorID::Buttonshadow:
+ case ColorID::Threeddarkshadow:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID) : NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+ case ColorID::Threedshadow:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID) : NS_RGB(0xE0, 0xE0, 0xE0);
+ break;
+ case ColorID::Threedface:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID) : NS_RGB(0xF0, 0xF0, 0xF0);
+ break;
+ case ColorID::Threedlightshadow:
+ case ColorID::Buttonborder:
+ case ColorID::MozDisabledfield:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID) : NS_RGB(0xDA, 0xDA, 0xDA);
+ break;
+ case ColorID::Menu:
+ color = GetColorFromNSColor(NSColor.textBackgroundColor);
+ break;
+ case ColorID::Windowframe:
+ color = GetColorFromNSColor(NSColor.windowFrameColor);
+ break;
+ case ColorID::Window: {
+ if (@available(macOS 10.14, *)) {
+ color = GetColorFromNSColor(NSColor.windowBackgroundColor);
+ } else {
+ // On 10.13 and below, NSColor.windowBackgroundColor is transparent black.
+ // Use a light grey instead (taken from macOS 11.5).
+ color = NS_RGB(0xF6, 0xF6, 0xF6);
+ }
+ break;
+ }
+ case ColorID::Field:
+ case ColorID::MozCombobox:
+ case ColorID::Inactiveborder:
+ case ColorID::Inactivecaption:
+ case ColorID::MozDialog:
+ color = GetColorFromNSColor(NSColor.controlBackgroundColor);
+ break;
+ case ColorID::Fieldtext:
+ case ColorID::MozComboboxtext:
+ case ColorID::Buttontext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::MozDialogtext:
+ case ColorID::MozCellhighlighttext:
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ color = GetColorFromNSColor(NSColor.controlTextColor);
+ break;
+ case ColorID::MozDragtargetzone:
+ color = GetColorFromNSColor(NSColor.selectedControlColor);
+ break;
+ case ColorID::MozMacChromeActive: {
+ int grey = NativeGreyColorAsInt(toolbarFillGrey, true);
+ color = NS_RGB(grey, grey, grey);
+ break;
+ }
+ case ColorID::MozMacChromeInactive: {
+ int grey = NativeGreyColorAsInt(toolbarFillGrey, false);
+ color = NS_RGB(grey, grey, grey);
+ break;
+ }
+ case ColorID::MozMacFocusring:
+ color = GetColorFromNSColorWithCustomAlpha(NSColor.keyboardFocusIndicatorColor, 0.48);
+ break;
+ case ColorID::MozMacMenushadow:
+ color = NS_RGB(0xA3, 0xA3, 0xA3);
+ break;
+ case ColorID::MozMacMenutextdisable:
+ color = NS_RGB(0x98, 0x98, 0x98);
+ break;
+ case ColorID::MozMacMenutextselect:
+ color = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
+ break;
+ case ColorID::MozMacDisabledtoolbartext:
+ case ColorID::Graytext:
+ color = GetColorFromNSColor(NSColor.disabledControlTextColor);
+ break;
+ case ColorID::MozMacMenuselect:
+ color = GetColorFromNSColor(NSColor.alternateSelectedControlColor);
+ break;
+ case ColorID::MozButtondefault:
+ color = NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+ case ColorID::MozCellhighlight:
+ case ColorID::MozMacSecondaryhighlight:
+ // For inactive list selection
+ color = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
+ break;
+ case ColorID::MozEventreerow:
+ // Background color of even list rows.
+ color = GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
+ break;
+ case ColorID::MozOddtreerow:
+ // Background color of odd list rows.
+ color = GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
+ break;
+ case ColorID::MozNativehyperlinktext:
+ color = GetColorFromNSColor(NSColor.linkColor);
+ break;
+ case ColorID::MozNativevisitedhyperlinktext:
+ color = GetColorFromNSColor(NSColor.systemPurpleColor);
+ break;
+ case ColorID::MozMacTooltip:
+ case ColorID::MozMacMenupopup:
+ case ColorID::MozMacMenuitem:
+ color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6) : NS_RGB(0x28, 0x28, 0x28);
+ break;
+ case ColorID::MozMacSourceList:
+ color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6) : NS_RGB(0x2d, 0x2d, 0x2d);
+ break;
+ case ColorID::MozMacSourceListSelection:
+ color = aScheme == ColorScheme::Light ? NS_RGB(0xd3, 0xd3, 0xd3) : NS_RGB(0x2d, 0x2d, 0x2d);
+ break;
+ case ColorID::MozMacActiveMenuitem:
+ case ColorID::MozMacActiveSourceListSelection:
+ case ColorID::Accentcolor:
+ color = GetColorFromNSColor(ControlAccentColor());
+ break;
+ case ColorID::Marktext:
+ case ColorID::Mark:
+ case ColorID::SpellCheckerUnderline:
+ aColor = GetStandinForNativeColor(aID, aScheme);
+ return NS_OK;
+ default:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ return NS_ERROR_FAILURE;
+ }
+
+ aColor = color;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK
+}
+
+nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ break;
+ case IntID::CaretBlinkTime:
+ aResult = 567;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ // Select textfield content when focused by kbd
+ // used by EventStateManager::sTextfieldSelectModel
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ // xul popups are not allowed to overlap the menubar.
+ aResult = 0;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ aResult = 4;
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrow_None;
+ break;
+ case IntID::UseOverlayScrollbars:
+ case IntID::AllowOverlayScrollbarsOverlap:
+ aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
+ break;
+ case IntID::ScrollbarDisplayOnMouseMove:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 450;
+ break;
+ case IntID::ScrollbarFadeDuration:
+ aResult = 200;
+ break;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::MacGraphiteTheme:
+ aResult = NSColor.currentControlTint == NSGraphiteControlTint;
+ break;
+ case IntID::MacBigSurTheme:
+ aResult = nsCocoaFeatures::OnBigSurOrLater();
+ break;
+ case IntID::MacRTL:
+ aResult = IsSystemOrientationRTL();
+ break;
+ case IntID::AlertNotificationOrigin:
+ aResult = NS_ALERT_TOP;
+ break;
+ case IntID::TabFocusModel:
+ aResult = [NSApp isFullKeyboardAccessEnabled] ? nsIContent::eTabFocus_any
+ : nsIContent::eTabFocus_textControlsMask;
+ break;
+ case IntID::ScrollToClick: {
+ aResult = [[NSUserDefaults standardUserDefaults] boolForKey:@"AppleScrollerPagingBehavior"];
+ } break;
+ case IntID::ChosenMenuItemsShouldBlink:
+ aResult = 1;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ aResult = -6;
+ break;
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 1;
+ break;
+ case IntID::SystemUsesDarkTheme:
+ aResult = SystemWantsDarkTheme();
+ break;
+ case IntID::PrefersReducedMotion:
+ aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
+ break;
+ case IntID::PrefersReducedTransparency:
+ aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceTransparency;
+ break;
+ case IntID::InvertedColors:
+ aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
+ break;
+ case IntID::UseAccessibilityTheme:
+ aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldIncreaseContrast;
+ break;
+ case IntID::VideoDynamicRange: {
+ // If the platform says it supports HDR, then we claim to support video-dynamic-range.
+ gfxPlatform* platform = gfxPlatform::GetPlatform();
+ MOZ_ASSERT(platform);
+ aResult = platform->SupportsHDR();
+ break;
+ }
+ case IntID::PanelAnimations:
+ aResult = 1;
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ }
+ return res;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ case FloatID::CursorScale: {
+ id uaDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.universalaccess"];
+ float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
+ [uaDefaults release];
+ aResult = f > 0.0 ? f : 1.0; // default to 1.0 if value not available
+ break;
+ }
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ }
+
+ return res;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsLookAndFeel::SystemWantsDarkTheme() {
+ // This returns true if the macOS system appearance is set to dark mode on
+ // 10.14+, false otherwise.
+ if (@available(macOS 10.14, *)) {
+ NSAppearanceName aquaOrDarkAqua = [NSApp.effectiveAppearance
+ bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]];
+ return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
+ }
+ return false;
+}
+
+/*static*/
+bool nsLookAndFeel::IsSystemOrientationRTL() {
+ NSWindow* window = [[NSWindow alloc] initWithContentRect:NSZeroRect
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ auto direction = window.windowTitlebarLayoutDirection;
+ [window release];
+ return direction == NSUserInterfaceLayoutDirectionRightToLeft;
+}
+
+bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName, gfxFontStyle& aFontStyle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoCString name;
+ gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
+ aFontName.Append(NS_ConvertUTF8toUTF16(name));
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void nsLookAndFeel::RecordAccessibilityTelemetry() {
+ if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector(accessibilityDisplayShouldInvertColors)]) {
+ bool val = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
+ Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INVERT_COLORS, val);
+ }
+}
+
+@implementation MOZLookAndFeelDynamicChangeObserver
+
++ (void)startObserving {
+ static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
+ if (!gInstance) {
+ gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init]; // leaked
+ }
+}
+
+- (instancetype)init {
+ self = [super init];
+
+ [NSNotificationCenter.defaultCenter addObserver:self
+ selector:@selector(colorsChanged)
+ name:NSControlTintDidChangeNotification
+ object:nil];
+ [NSNotificationCenter.defaultCenter addObserver:self
+ selector:@selector(colorsChanged)
+ name:NSSystemColorsDidChangeNotification
+ object:nil];
+
+ if (@available(macOS 10.14, *)) {
+ [NSWorkspace.sharedWorkspace.notificationCenter
+ addObserver:self
+ selector:@selector(mediaQueriesChanged)
+ name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
+ object:nil];
+ } else {
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(mediaQueriesChanged)
+ name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
+ object:nil];
+ }
+
+ [NSNotificationCenter.defaultCenter addObserver:self
+ selector:@selector(scrollbarsChanged)
+ name:NSPreferredScrollerStyleDidChangeNotification
+ object:nil];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(scrollbarsChanged)
+ name:@"AppleAquaScrollBarVariantChanged"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(cachedValuesChanged)
+ name:@"AppleNoRedisplayAppearancePreferenceChanged"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(cachedValuesChanged)
+ name:@"com.apple.KeyboardUIModeDidChange"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
+
+ [MOZGlobalAppearance.sharedInstance addObserver:self
+ forKeyPath:@"effectiveAppearance"
+ options:0
+ context:nil];
+ [NSApp addObserver:self forKeyPath:@"effectiveAppearance" options:0 context:nil];
+
+ return self;
+}
+
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id>*)change
+ context:(void*)context {
+ if ([keyPath isEqualToString:@"effectiveAppearance"]) {
+ [self entireThemeChanged];
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+- (void)entireThemeChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
+}
+
+- (void)scrollbarsChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
+}
+
+- (void)mediaQueriesChanged {
+ // Changing`Invert Colors` sends AccessibilityDisplayOptionsDidChangeNotifications.
+ // We monitor that setting via telemetry, so call into that
+ // recording method here.
+ nsLookAndFeel::RecordAccessibilityTelemetry();
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::MediaQueriesOnly);
+}
+
+- (void)colorsChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
+}
+
+- (void)cachedValuesChanged {
+ // We only need to re-cache (and broadcast) updated LookAndFeel values, so that they're
+ // up-to-date the next time they're queried. No further change handling is needed.
+ // TODO: Add a change hint for this which avoids the unnecessary media query invalidation.
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::MediaQueriesOnly);
+}
+@end
diff --git a/widget/cocoa/nsMacCursor.h b/widget/cocoa/nsMacCursor.h
new file mode 100644
index 0000000000..fc97728da5
--- /dev/null
+++ b/widget/cocoa/nsMacCursor.h
@@ -0,0 +1,128 @@
+/* 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/. */
+
+#ifndef nsMacCursor_h_
+#define nsMacCursor_h_
+
+#import <Cocoa/Cocoa.h>
+#import "nsIWidget.h"
+
+/*! @class nsMacCursor
+ @abstract Represents a native Mac cursor.
+ @discussion <code>nsMacCursor</code> provides a simple API for creating and
+ working with native Macintosh cursors. Cursors can be created
+ used without needing to be aware of the way different cursors
+ are implemented, in particular the details of managing an
+ animated cursor are hidden.
+*/
+@interface nsMacCursor : NSObject {
+ @private
+ NSTimer* mTimer;
+ @protected
+ nsCursor mType;
+ int mFrameCounter;
+}
+
+/*! @method cursorWithCursor:
+ @abstract Create a cursor by specifying a Cocoa <code>NSCursor</code>.
+ @discussion Creates a cursor representing the given Cocoa built-in cursor.
+ @param aCursor the <code>NSCursor</code> to use
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code>
+ representing the given <code>NSCursor</code>
+ */
++ (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType;
+
+/*! @method cursorWithImageNamed:hotSpot:type:
+ @abstract Create a cursor by specifying the name of an image resource to
+ use for the cursor and a hotspot.
+ @discussion Creates a cursor by loading the named image using the
+ <code>+[NSImage imageNamed:]</code> method.
+ <p>The image must be compatible with any restrictions laid down
+ by <code>NSCursor</code>. These vary by operating system
+ version.</p>
+ <p>The hotspot precisely determines the point where the user
+ clicks when using the cursor.</p>
+ @param aCursor the name of the image to use for the cursor
+ @param aPoint the point within the cursor to use as the hotspot
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code> that uses the given image and
+ hotspot
+ */
++ (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType;
+
+/*! @method cursorWithFrames:type:
+ @abstract Create an animated cursor by specifying the frames to use for
+ the animation.
+ @discussion Creates a cursor that will animate by cycling through the given
+ frames. Each element of the array must be an instance of
+ <code>NSCursor</code>
+ @param aCursorFrames an array of <code>NSCursor</code>, representing
+ the frames of an animated cursor, in the order they should be
+ played.
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code> that will
+ animate the given cursor frames
+ */
++ (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType;
+
+/*! @method cocoaCursorWithImageNamed:hotSpot:
+ @abstract Create a Cocoa NSCursor object with a Gecko image resource name
+ and a hotspot point.
+ @discussion Create a Cocoa NSCursor object with a Gecko image resource name
+ and a hotspot point.
+ @param imageName the name of the gecko image resource, "tiff"
+ extension is assumed, do not append.
+ @param aPoint the point within the cursor to use as the hotspot
+ @result an autoreleased instance of <code>nsMacCursor</code> that will
+ animate the given cursor frames
+ */
++ (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName hotSpot:(NSPoint)aPoint;
+
+/*! @method isSet
+ @abstract Determines whether this cursor is currently active.
+ @discussion This can be helpful when the Cocoa NSCursor state can be
+ influenced without going through nsCursorManager.
+ @result whether the cursor is currently set
+ */
+- (BOOL)isSet;
+
+/*! @method set
+ @abstract Set the cursor.
+ @discussion Makes this cursor the current cursor. If the cursor is
+ animated, the animation is started.
+ */
+- (void)set;
+
+/*! @method unset
+ @abstract Unset the cursor. The cursor will return to the default
+ (usually the arrow cursor).
+ @discussion Unsets the cursor. If the cursor is animated, the animation is
+ stopped.
+ */
+- (void)unset;
+
+/*! @method isAnimated
+ @abstract Tests whether this cursor is animated.
+ @discussion Use this method to determine whether a cursor is animated
+ @result YES if the cursor is animated (has more than one frame), NO if
+ it is a simple static cursor.
+ */
+- (BOOL)isAnimated;
+
+/** @method cursorType
+ @abstract Get the cursor type for this cursor
+ @discussion This method returns the <code>nsCursor</code> constant that
+ corresponds to this cursor, which is equivalent to the CSS
+ name for the cursor.
+ @result The nsCursor constant corresponding to this cursor, or
+ nsCursor's 'eCursorCount' if the cursor is a custom cursor
+ loaded from a URI
+ */
+- (nsCursor)type;
+@end
+
+#endif // nsMacCursor_h_
diff --git a/widget/cocoa/nsMacCursor.mm b/widget/cocoa/nsMacCursor.mm
new file mode 100644
index 0000000000..0f56cf9a27
--- /dev/null
+++ b/widget/cocoa/nsMacCursor.mm
@@ -0,0 +1,367 @@
+/* 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/. */
+
+#include "nsMacCursor.h"
+#include "nsObjCExceptions.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+/*! @category nsMacCursor (PrivateMethods)
+ @abstract Private methods internal to the nsMacCursor class.
+ @discussion <code>nsMacCursor</code> is effectively an abstract class. It does not define
+ complete behaviour in and of itself, the subclasses defined in this file provide the useful
+ implementations.
+*/
+@interface nsMacCursor (PrivateMethods)
+
+/*! @method getNextCursorFrame
+ @abstract get the index of the next cursor frame to display.
+ @discussion Increments and returns the frame counter of an animated cursor.
+ @result The index of the next frame to display in the cursor animation
+*/
+- (int)getNextCursorFrame;
+
+/*! @method numFrames
+ @abstract Query the number of frames in this cursor's animation.
+ @discussion Returns the number of frames in this cursor's animation. Static cursors return 1.
+*/
+- (int)numFrames;
+
+/*! @method createTimer
+ @abstract Create a Timer to use to animate the cursor.
+ @discussion Creates an instance of <code>NSTimer</code> which is used to drive the cursor
+ animation. This method should only be called for cursors that are animated.
+*/
+- (void)createTimer;
+
+/*! @method destroyTimer
+ @abstract Destroy any timer instance associated with this cursor.
+ @discussion Invalidates and releases any <code>NSTimer</code> instance associated with this
+ cursor.
+ */
+- (void)destroyTimer;
+/*! @method destroyTimer
+ @abstract Destroy any timer instance associated with this cursor.
+ @discussion Invalidates and releases any <code>NSTimer</code> instance associated with this
+ cursor.
+*/
+
+/*! @method advanceAnimatedCursor:
+ @abstract Method called by animation timer to perform animation.
+ @discussion Called by an animated cursor's associated timer to advance the animation to the next
+ frame. Determines which frame should occur next and sets the cursor to that frame.
+ @param aTimer the timer causing the animation
+*/
+- (void)advanceAnimatedCursor:(NSTimer*)aTimer;
+
+/*! @method setFrame:
+ @abstract Sets the current cursor, using an index to determine which frame in the animation to
+ display.
+ @discussion Sets the current cursor. The frame index determines which frame is shown if the
+ cursor is animated. Frames and numbered from <code>0</code> to <code>-[nsMacCursor numFrames] -
+ 1</code>. A static cursor has a single frame, numbered 0.
+ @param aFrameIndex the index indicating which frame from the animation to display
+*/
+- (void)setFrame:(int)aFrameIndex;
+
+@end
+
+/*! @class nsCocoaCursor
+ @abstract Implementation of <code>nsMacCursor</code> that uses Cocoa <code>NSCursor</code>
+ instances.
+ @discussion Displays a static or animated cursor, using Cocoa <code>NSCursor</code> instances.
+ These can be either built-in <code>NSCursor</code> instances, or custom <code>NSCursor</code>s
+ created from images. When more than one <code>NSCursor</code> is provided, the cursor will use
+ these as animation frames.
+*/
+@interface nsCocoaCursor : nsMacCursor {
+ @private
+ NSArray* mFrames;
+ NSCursor* mLastSetCocoaCursor;
+}
+
+/*! @method initWithFrames:
+ @abstract Create an animated cursor by specifying the frames to use for the animation.
+ @discussion Creates a cursor that will animate by cycling through the given frames. Each element
+ of the array must be an instance of <code>NSCursor</code>
+ @param aCursorFrames an array of <code>NSCursor</code>, representing the frames of an
+ animated cursor, in the order they should be played.
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> that will animate the given cursor frames
+ */
+- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType;
+
+/*! @method initWithCursor:
+ @abstract Create a cursor by specifying a Cocoa <code>NSCursor</code>.
+ @discussion Creates a cursor representing the given Cocoa built-in cursor.
+ @param aCursor the <code>NSCursor</code> to use
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> representing the given
+ <code>NSCursor</code>
+*/
+- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType;
+
+/*! @method initWithImageNamed:hotSpot:
+ @abstract Create a cursor by specifying the name of an image resource to use for the cursor
+ and a hotspot.
+ @discussion Creates a cursor by loading the named image using the <code>+[NSImage
+ imageNamed:]</code> method. <p>The image must be compatible with any restrictions laid down by
+ <code>NSCursor</code>. These vary by operating system version.</p> <p>The hotspot precisely
+ determines the point where the user clicks when using the cursor.</p>
+ @param aCursor the name of the image to use for the cursor
+ @param aPoint the point within the cursor to use as the hotspot
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> that uses the given image and hotspot
+*/
+- (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType;
+
+@end
+
+@implementation nsMacCursor
+
++ (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithCursor:aCursor type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithImageNamed:aCursorImage hotSpot:aPoint
+ type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithFrames:aCursorFrames type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName hotSpot:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCOMPtr<nsIFile> resDir;
+ nsAutoCString resPath;
+ NSString *pathToImage, *pathToHiDpiImage;
+ NSImage *cursorImage, *hiDpiCursorImage;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(resDir));
+ if (NS_FAILED(rv)) goto INIT_FAILURE;
+ resDir->AppendNative("res"_ns);
+ resDir->AppendNative("cursors"_ns);
+
+ rv = resDir->GetNativePath(resPath);
+ if (NS_FAILED(rv)) goto INIT_FAILURE;
+
+ pathToImage = [NSString stringWithUTF8String:(const char*)resPath.get()];
+ if (!pathToImage) goto INIT_FAILURE;
+ pathToImage = [pathToImage stringByAppendingPathComponent:imageName];
+ pathToHiDpiImage = [pathToImage stringByAppendingString:@"@2x"];
+ // Add same extension to both image paths.
+ pathToImage = [pathToImage stringByAppendingPathExtension:@"png"];
+ pathToHiDpiImage = [pathToHiDpiImage stringByAppendingPathExtension:@"png"];
+
+ cursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToImage] autorelease];
+ if (!cursorImage) goto INIT_FAILURE;
+
+ // Note 1: There are a few different ways to get a hidpi image via
+ // initWithContentsOfFile. We let the OS handle this here: when the
+ // file basename ends in "@2x", it will be displayed at native resolution
+ // instead of being pixel-doubled. See bug 784909 comment 7 for alternates ways.
+ //
+ // Note 2: The OS is picky, and will ignore the hidpi representation
+ // unless it is exactly twice the size of the lowdpi image.
+ hiDpiCursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToHiDpiImage] autorelease];
+ if (hiDpiCursorImage) {
+ NSImageRep* imageRep = [[hiDpiCursorImage representations] objectAtIndex:0];
+ [cursorImage addRepresentation:imageRep];
+ }
+ return [[[NSCursor alloc] initWithImage:cursorImage hotSpot:aPoint] autorelease];
+
+INIT_FAILURE:
+ NS_WARNING("Problem getting path to cursor image file!");
+ [self release];
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isSet {
+ // implemented by subclasses
+ return NO;
+}
+
+- (void)set {
+ if ([self isAnimated]) {
+ [self createTimer];
+ }
+ // if the cursor isn't animated or the timer creation fails for any reason...
+ if (!mTimer) {
+ [self setFrame:0];
+ }
+}
+
+- (void)unset {
+ [self destroyTimer];
+}
+
+- (BOOL)isAnimated {
+ return [self numFrames] > 1;
+}
+
+- (int)numFrames {
+ // subclasses need to override this to support animation
+ return 1;
+}
+
+- (int)getNextCursorFrame {
+ mFrameCounter = (mFrameCounter + 1) % [self numFrames];
+ return mFrameCounter;
+}
+
+- (void)createTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mTimer) {
+ mTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25
+ target:self
+ selector:@selector(advanceAnimatedCursor:)
+ userInfo:nil
+ repeats:YES] retain];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)destroyTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTimer) {
+ [mTimer invalidate];
+ [mTimer release];
+ mTimer = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)advanceAnimatedCursor:(NSTimer*)aTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([aTimer isValid]) {
+ [self setFrame:[self getNextCursorFrame]];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)setFrame:(int)aFrameIndex {
+ // subclasses need to do something useful here
+}
+
+- (nsCursor)type {
+ return mType;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self destroyTimer];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@implementation nsCocoaCursor
+
+- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ self = [super init];
+ NSEnumerator* it = [aCursorFrames objectEnumerator];
+ NSObject* frame = nil;
+ while ((frame = [it nextObject])) {
+ NS_ASSERTION([frame isKindOfClass:[NSCursor class]],
+ "Invalid argument: All frames must be of type NSCursor");
+ }
+ mFrames = [aCursorFrames retain];
+ mFrameCounter = 0;
+ mType = aType;
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSArray* frame = [NSArray arrayWithObjects:aCursor, nil];
+ return [self initWithFrames:frame type:aType];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [self initWithCursor:[nsMacCursor cocoaCursorWithImageNamed:aCursorImage hotSpot:aPoint]
+ type:aType];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isSet {
+ return [NSCursor currentCursor] == mLastSetCocoaCursor;
+}
+
+- (void)setFrame:(int)aFrameIndex {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSCursor* newCursor = [mFrames objectAtIndex:aFrameIndex];
+ [newCursor set];
+ mLastSetCocoaCursor = newCursor;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (int)numFrames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mFrames count];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0);
+}
+
+- (NSString*)description {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mFrames description];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mFrames release];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/nsMacDockSupport.h b/widget/cocoa/nsMacDockSupport.h
new file mode 100644
index 0000000000..f3a12485b3
--- /dev/null
+++ b/widget/cocoa/nsMacDockSupport.h
@@ -0,0 +1,35 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#include "nsIMacDockSupport.h"
+#include "nsIStandaloneNativeMenu.h"
+#include "nsITaskbarProgress.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+@class MOZProgressDockOverlayView;
+
+class nsMacDockSupport : public nsIMacDockSupport, public nsITaskbarProgress {
+ public:
+ nsMacDockSupport();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACDOCKSUPPORT
+ NS_DECL_NSITASKBARPROGRESS
+
+ protected:
+ virtual ~nsMacDockSupport();
+
+ nsCOMPtr<nsIStandaloneNativeMenu> mDockMenu;
+ nsString mBadgeText;
+
+ NSView* mDockTileWrapperView;
+ MOZProgressDockOverlayView* mProgressDockOverlayView;
+
+ nsTaskbarProgressState mProgressState;
+ double mProgressFraction;
+
+ nsresult UpdateDockTile();
+};
diff --git a/widget/cocoa/nsMacDockSupport.mm b/widget/cocoa/nsMacDockSupport.mm
new file mode 100644
index 0000000000..e69c0b6259
--- /dev/null
+++ b/widget/cocoa/nsMacDockSupport.mm
@@ -0,0 +1,419 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <signal.h>
+
+#include "nsCocoaUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMacDockSupport.h"
+#include "nsObjCExceptions.h"
+#include "nsNativeThemeColors.h"
+#include "nsString.h"
+
+NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
+
+// This view is used in the dock tile when we're downloading a file.
+// It draws a progress bar that looks similar to the native progress bar on
+// 10.12. This style of progress bar is not animated, unlike the pre-10.10
+// progress bar look which had to redrawn multiple times per second.
+@interface MOZProgressDockOverlayView : NSView {
+ double mFractionValue;
+}
+@property double fractionValue;
+
+@end
+
+@implementation MOZProgressDockOverlayView
+
+@synthesize fractionValue = mFractionValue;
+
+- (void)drawRect:(NSRect)aRect {
+ // Erase the background behind this view, i.e. cut a rectangle hole in the icon.
+ [[NSColor clearColor] set];
+ NSRectFill(self.bounds);
+
+ // Split the height of this view into four quarters. The middle two quarters
+ // will be covered by the actual progress bar.
+ CGFloat radius = self.bounds.size.height / 4;
+ NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
+
+ NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
+ xRadius:radius
+ yRadius:radius];
+
+ // Draw a grayish background first.
+ [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
+ [path fill];
+
+ // Draw a fill in the control accent color for the progress part.
+ NSRect progressFillRect = self.bounds;
+ progressFillRect.size.width *= mFractionValue;
+ [NSGraphicsContext saveGraphicsState];
+ [NSBezierPath clipRect:progressFillRect];
+ [ControlAccentColor() setFill];
+ [path fill];
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Add a shadowy stroke on top.
+ [NSGraphicsContext saveGraphicsState];
+ [path addClip];
+ [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
+ path.lineWidth = barBounds.size.height / 10;
+ [path stroke];
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
+
+nsMacDockSupport::nsMacDockSupport()
+ : mDockTileWrapperView(nil),
+ mProgressDockOverlayView(nil),
+ mProgressState(STATE_NO_PROGRESS),
+ mProgressFraction(0.0) {}
+
+nsMacDockSupport::~nsMacDockSupport() {
+ if (mDockTileWrapperView) {
+ [mDockTileWrapperView release];
+ mDockTileWrapperView = nil;
+ }
+ if (mProgressDockOverlayView) {
+ [mProgressDockOverlayView release];
+ mProgressDockOverlayView = nil;
+ }
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
+ nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
+ dockMenu.forget(aDockMenu);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
+ mDockMenu = aDockMenu;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [[NSApplication sharedApplication] activateIgnoringOtherApps:aIgnoreOtherApplications];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
+ mBadgeText = aBadgeText;
+ if (aBadgeText.IsEmpty())
+ [tile setBadgeLabel:nil];
+ else
+ [tile setBadgeLabel:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(mBadgeText.get())
+ length:mBadgeText.Length()]];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
+ aBadgeText = mBadgeText;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState, uint64_t aCurrentValue,
+ uint64_t aMaxValue) {
+ NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
+ if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
+ NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
+ }
+ if (aCurrentValue > aMaxValue) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ mProgressState = aState;
+ if (aMaxValue == 0) {
+ mProgressFraction = 0;
+ } else {
+ mProgressFraction = (double)aCurrentValue / aMaxValue;
+ }
+
+ return UpdateDockTile();
+}
+
+nsresult nsMacDockSupport::UpdateDockTile() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
+ if (!mDockTileWrapperView) {
+ // Create the following NSView hierarchy:
+ // * mDockTileWrapperView (NSView)
+ // * imageView (NSImageView) <- has the application icon
+ // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the progress bar
+
+ mDockTileWrapperView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
+ mDockTileWrapperView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+
+ NSImageView* imageView = [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
+ imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
+ imageView.imageScaling = NSImageScaleAxesIndependently;
+ imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ [mDockTileWrapperView addSubview:imageView];
+
+ mProgressDockOverlayView =
+ [[MOZProgressDockOverlayView alloc] initWithFrame:NSMakeRect(1, 3, 30, 4)];
+ mProgressDockOverlayView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
+ NSViewMaxXMargin | NSViewMinYMargin |
+ NSViewHeightSizable | NSViewMaxYMargin;
+ [mDockTileWrapperView addSubview:mProgressDockOverlayView];
+ }
+ if (NSApp.dockTile.contentView != mDockTileWrapperView) {
+ NSApp.dockTile.contentView = mDockTileWrapperView;
+ }
+
+ if (mProgressState == STATE_NORMAL) {
+ mProgressDockOverlayView.fractionValue = mProgressFraction;
+ } else {
+ // Indeterminate states are rare. Just fill the entire progress bar in
+ // that case.
+ mProgressDockOverlayView.fractionValue = 1.0;
+ }
+ [NSApp.dockTile display];
+ } else if (NSApp.dockTile.contentView) {
+ NSApp.dockTile.contentView = nil;
+ [NSApp.dockTile display];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+extern "C" {
+// Private CFURL API used by the Dock.
+CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
+CFURLRef _CFURLCreateFromPropertyListRepresentation(CFAllocatorRef alloc,
+ CFPropertyListRef pListRepresentation);
+} // extern "C"
+
+namespace {
+
+const NSArray* const browserAppNames =
+ [NSArray arrayWithObjects:@"Firefox.app", @"Firefox Beta.app", @"Firefox Nightly.app",
+ @"Safari.app", @"WebKit.app", @"Google Chrome.app",
+ @"Google Chrome Canary.app", @"Chromium.app", @"Opera.app", nil];
+
+constexpr NSString* const kDockDomainName = @"com.apple.dock";
+// See https://developer.apple.com/documentation/devicemanagement/dock
+constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
+// See https://developer.apple.com/documentation/devicemanagement/dock/staticitem
+constexpr NSString* const kDockTileDataKey = @"tile-data";
+constexpr NSString* const kDockFileDataKey = @"file-data";
+
+NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
+ if (!aDockPlist) {
+ return nil;
+ }
+ NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
+ if (![persistentApps isKindOfClass:[NSArray class]]) {
+ return nil;
+ }
+ return persistentApps;
+}
+
+NSString* GetPathForApp(NSDictionary* aPersistantApp) {
+ if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
+ if (![tileData isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* fileData = tileData[kDockFileDataKey];
+ if (![fileData isKindOfClass:[NSDictionary class]]) {
+ // Some special tiles may not have DockFileData but we can ignore those.
+ return nil;
+ }
+ NSURL* url = CFBridgingRelease(_CFURLCreateFromPropertyListRepresentation(NULL, fileData));
+ if (!url) {
+ return nil;
+ }
+ return [url isFileURL] ? [url path] : nullptr;
+}
+
+// The only reliable way to get our changes to take effect seems to be to use
+// `kill`.
+void RefreshDock(NSDictionary* aDockPlist) {
+ [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist forName:kDockDomainName];
+ NSRunningApplication* dockApp = [[NSRunningApplication
+ runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
+ if (!dockApp) {
+ return;
+ }
+ pid_t pid = [dockApp processIdentifier];
+ if (pid > 0) {
+ kill(pid, SIGTERM);
+ }
+}
+
+} // namespace
+
+nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aIsInDock = false;
+
+ NSDictionary* dockPlist =
+ [[NSUserDefaults standardUserDefaults] persistentDomainForName:kDockDomainName];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSString* appPath = [[NSBundle mainBundle] bundlePath];
+
+ for (id app in persistentApps) {
+ NSString* persistentAppPath = GetPathForApp(app);
+ if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
+ *aIsInDock = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(const nsAString& aAppPath,
+ const nsAString& aAppToReplacePath,
+ bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
+
+ *aIsInDock = false;
+
+ NSString* appPath =
+ !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath) : [[NSBundle mainBundle] bundlePath];
+ NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
+
+ NSMutableDictionary* dockPlist =
+ [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
+ persistentDomainForName:kDockDomainName]];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSMutableArray* persistentApps =
+ [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // See the comment for this method in the .idl file for the strategy that we
+ // use here to determine where to pin the app.
+ NSUInteger preexistingAppIndex = NSNotFound; // full path matches
+ NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
+ NSUInteger toReplaceAppIndex = NSNotFound;
+ NSUInteger lastBrowserAppIndex = NSNotFound;
+ for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
+ NSString* persistentAppPath = GetPathForApp([persistentApps objectAtIndex:index]);
+
+ if ([persistentAppPath isEqualToString:appPath]) {
+ preexistingAppIndex = index;
+ } else if (appToReplacePath && [persistentAppPath isEqualToString:appToReplacePath]) {
+ toReplaceAppIndex = index;
+ } else {
+ NSString* appName = [appPath lastPathComponent];
+ NSString* persistentAppName = [persistentAppPath lastPathComponent];
+
+ if ([persistentAppName isEqual:appName]) {
+ if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
+ [appToReplacePath containsString:@"/AppTranslocation/"] &&
+ [persistentAppPath hasPrefix:@"/Volumes/"]) {
+ // This is a special case when an app with the same name was
+ // previously dragged and pinned from a quarantined DMG straight to
+ // the Dock and an attempt is now made to pin the same named app to
+ // the Dock. In this case we want to replace the currently pinned app
+ // icon.
+ toReplaceAppIndex = index;
+ } else {
+ sameNameAppIndex = index;
+ }
+ } else {
+ if ([browserAppNames containsObject:persistentAppName]) {
+ lastBrowserAppIndex = index;
+ }
+ }
+ }
+ }
+
+ // Special cases where we're not going to add a new Dock tile:
+ if (preexistingAppIndex != NSNotFound) {
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps removeObjectAtIndex:toReplaceAppIndex];
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+ }
+ *aIsInDock = true;
+ return NS_OK;
+ }
+
+ // Create new tile:
+ NSDictionary* newDockTile = nullptr;
+ {
+ NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
+ NSDictionary* dict =
+ CFBridgingRelease(_CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
+ if (!dict) {
+ return NS_ERROR_FAILURE;
+ }
+ NSDictionary* dockTileData = [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
+ if (dockTileData) {
+ newDockTile = [NSDictionary dictionaryWithObject:dockTileData forKey:kDockTileDataKey];
+ }
+ if (!newDockTile) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Update the Dock:
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps replaceObjectAtIndex:toReplaceAppIndex withObject:newDockTile];
+ } else {
+ NSUInteger index;
+ if (sameNameAppIndex != NSNotFound) {
+ index = sameNameAppIndex + 1;
+ } else if (lastBrowserAppIndex != NSNotFound) {
+ index = lastBrowserAppIndex + 1;
+ } else {
+ index = [persistentApps count];
+ }
+ [persistentApps insertObject:newDockTile atIndex:index];
+ }
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+
+ *aIsInDock = true;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacFinderProgress.h b/widget/cocoa/nsMacFinderProgress.h
new file mode 100644
index 0000000000..a0e48a0d59
--- /dev/null
+++ b/widget/cocoa/nsMacFinderProgress.h
@@ -0,0 +1,24 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+#ifndef _MACFINDERPROGRESS_H_
+#define _MACFINDERPROGRESS_H_
+
+#include "nsIMacFinderProgress.h"
+#include "nsCOMPtr.h"
+
+class nsMacFinderProgress : public nsIMacFinderProgress {
+ public:
+ nsMacFinderProgress();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACFINDERPROGRESS
+
+ protected:
+ virtual ~nsMacFinderProgress();
+
+ NSProgress* mProgress;
+};
+
+#endif
diff --git a/widget/cocoa/nsMacFinderProgress.mm b/widget/cocoa/nsMacFinderProgress.mm
new file mode 100644
index 0000000000..2e518fe012
--- /dev/null
+++ b/widget/cocoa/nsMacFinderProgress.mm
@@ -0,0 +1,87 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsMacFinderProgress.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsString.h"
+#include "nsObjCExceptions.h"
+
+NS_IMPL_ISUPPORTS(nsMacFinderProgress, nsIMacFinderProgress)
+
+nsMacFinderProgress::nsMacFinderProgress() : mProgress(nil) {}
+
+nsMacFinderProgress::~nsMacFinderProgress() {
+ if (mProgress) {
+ [mProgress unpublish];
+ [mProgress release];
+ }
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::Init(const nsAString& path,
+ nsIMacFinderProgressCanceledCallback* cancellationCallback) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* pathUrl = [NSURL
+ fileURLWithPath:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(path.BeginReading())
+ length:path.Length()]];
+ NSDictionary* userInfo = @{
+ @"NSProgressFileOperationKindKey" : @"NSProgressFileOperationKindDownloading",
+ @"NSProgressFileURLKey" : pathUrl
+ };
+
+ mProgress = [[NSProgress alloc] initWithParent:nil userInfo:userInfo];
+ mProgress.kind = NSProgressKindFile;
+ mProgress.cancellable = YES;
+
+ nsMainThreadPtrHandle<nsIMacFinderProgressCanceledCallback> cancellationCallbackHandle(
+ new nsMainThreadPtrHolder<nsIMacFinderProgressCanceledCallback>(
+ "MacFinderProgress::CancellationCallback", cancellationCallback));
+
+ mProgress.cancellationHandler = ^{
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("MacFinderProgress::Canceled", [cancellationCallbackHandle] {
+ MOZ_ASSERT(NS_IsMainThread());
+ cancellationCallbackHandle->Canceled();
+ }));
+ };
+
+ [mProgress publish];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::UpdateProgress(uint64_t currentProgress, uint64_t totalProgress) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if (mProgress) {
+ mProgress.totalUnitCount = totalProgress;
+ mProgress.completedUnitCount = currentProgress;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::End() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mProgress) {
+ [mProgress unpublish];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacSharingService.h b/widget/cocoa/nsMacSharingService.h
new file mode 100644
index 0000000000..d97ce59380
--- /dev/null
+++ b/widget/cocoa/nsMacSharingService.h
@@ -0,0 +1,22 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsMacSharingService_h_
+#define nsMacSharingService_h_
+
+#include "nsIMacSharingService.h"
+
+class nsMacSharingService : public nsIMacSharingService {
+ public:
+ nsMacSharingService() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACSHARINGSERVICE
+
+ protected:
+ virtual ~nsMacSharingService() {}
+};
+
+#endif // nsMacSharingService_h_
diff --git a/widget/cocoa/nsMacSharingService.mm b/widget/cocoa/nsMacSharingService.mm
new file mode 100644
index 0000000000..bc62e5e85e
--- /dev/null
+++ b/widget/cocoa/nsMacSharingService.mm
@@ -0,0 +1,206 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsMacSharingService.h"
+
+#include "jsapi.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+#include "nsCocoaUtils.h"
+#include "mozilla/MacStringHelpers.h"
+
+NS_IMPL_ISUPPORTS(nsMacSharingService, nsIMacSharingService)
+
+NSString* const remindersServiceName = @"com.apple.reminders.RemindersShareExtension";
+
+// These are some undocumented constants also used by Safari
+// to let us open the preferences window
+NSString* const extensionPrefPanePath = @"/System/Library/PreferencePanes/Extensions.prefPane";
+const UInt32 openSharingSubpaneDescriptorType = 'ptru';
+NSString* const openSharingSubpaneActionKey = @"action";
+NSString* const openSharingSubpaneActionValue = @"revealExtensionPoint";
+NSString* const openSharingSubpaneProtocolKey = @"protocol";
+NSString* const openSharingSubpaneProtocolValue = @"com.apple.share-services";
+
+// Expose the id so we can pass reference through to JS and back
+@interface NSSharingService (ExposeName)
+- (id)name;
+@end
+
+// Filter providers that we do not want to expose to the user, because they are duplicates or do not
+// work correctly within the context
+static bool ShouldIgnoreProvider(NSString* aProviderName) {
+ return [aProviderName isEqualToString:@"com.apple.share.System.add-to-safari-reading-list"];
+}
+
+// Clean up the activity once the share is complete
+@interface SharingServiceDelegate : NSObject <NSSharingServiceDelegate> {
+ NSUserActivity* mShareActivity;
+}
+
+- (void)cleanup;
+
+@end
+
+@implementation SharingServiceDelegate
+
+- (id)initWithActivity:(NSUserActivity*)activity {
+ self = [super init];
+ mShareActivity = [activity retain];
+ return self;
+}
+
+- (void)cleanup {
+ [mShareActivity resignCurrent];
+ [mShareActivity invalidate];
+ [mShareActivity release];
+ mShareActivity = nil;
+}
+
+- (void)sharingService:(NSSharingService*)sharingService didShareItems:(NSArray*)items {
+ [self cleanup];
+}
+
+- (void)sharingService:(NSSharingService*)service
+ didFailToShareItems:(NSArray*)items
+ error:(NSError*)error {
+ [self cleanup];
+}
+
+- (void)dealloc {
+ [mShareActivity release];
+ [super dealloc];
+}
+
+@end
+
+static NSString* NSImageToBase64(const NSImage* aImage) {
+ CGImageRef cgRef = [aImage CGImageForProposedRect:nil context:nil hints:nil];
+ NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
+ [bitmapRep setSize:[aImage size]];
+ NSData* imageData = [bitmapRep representationUsingType:NSPNGFileType properties:@{}];
+ NSString* base64Encoded = [imageData base64EncodedStringWithOptions:0];
+ [bitmapRep release];
+ return [NSString stringWithFormat:@"data:image/png;base64,%@", base64Encoded];
+}
+
+static void SetStrAttribute(JSContext* aCx, JS::Rooted<JSObject*>& aObj, const char* aKey,
+ NSString* aVal) {
+ nsAutoString strVal;
+ mozilla::CopyCocoaStringToXPCOMString(aVal, strVal);
+ JS::Rooted<JSString*> title(aCx, JS_NewUCStringCopyZ(aCx, strVal.get()));
+ JS::Rooted<JS::Value> attVal(aCx, JS::StringValue(title));
+ JS_SetProperty(aCx, aObj, aKey, attVal);
+}
+
+nsresult nsMacSharingService::GetSharingProviders(const nsAString& aPageUrl, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* url = nsCocoaUtils::ToNSURL(aPageUrl);
+ if (!url) {
+ // aPageUrl is not a valid URL.
+ return NS_ERROR_FAILURE;
+ }
+
+ NSArray* sharingService = [NSSharingService sharingServicesForItems:@[ url ]];
+ int32_t serviceCount = 0;
+ JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0));
+
+ for (NSSharingService* currentService in sharingService) {
+ if (ShouldIgnoreProvider([currentService name])) {
+ continue;
+ }
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+
+ SetStrAttribute(aCx, obj, "name", [currentService name]);
+ SetStrAttribute(aCx, obj, "menuItemTitle", currentService.menuItemTitle);
+ SetStrAttribute(aCx, obj, "image", NSImageToBase64(currentService.image));
+
+ JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj));
+ JS_SetElement(aCx, array, serviceCount++, element);
+ }
+
+ aResult.setObject(*array);
+
+ return NS_OK;
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacSharingService::OpenSharingPreferences() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* prefPaneURL = [NSURL fileURLWithPath:extensionPrefPanePath isDirectory:YES];
+ NSDictionary* args = @{
+ openSharingSubpaneActionKey : openSharingSubpaneActionValue,
+ openSharingSubpaneProtocolKey : openSharingSubpaneProtocolValue
+ };
+ NSData* data = [NSPropertyListSerialization dataWithPropertyList:args
+ format:NSPropertyListXMLFormat_v1_0
+ options:0
+ error:nil];
+ NSAppleEventDescriptor* descriptor =
+ [[NSAppleEventDescriptor alloc] initWithDescriptorType:openSharingSubpaneDescriptorType
+ data:data];
+
+ [[NSWorkspace sharedWorkspace] openURLs:@[ prefPaneURL ]
+ withAppBundleIdentifier:nil
+ options:NSWorkspaceLaunchAsync
+ additionalEventParamDescriptor:descriptor
+ launchIdentifiers:NULL];
+
+ [descriptor release];
+
+ return NS_OK;
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacSharingService::ShareUrl(const nsAString& aServiceName, const nsAString& aPageUrl,
+ const nsAString& aPageTitle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* serviceName = nsCocoaUtils::ToNSString(aServiceName);
+ NSURL* pageUrl = nsCocoaUtils::ToNSURL(aPageUrl);
+ NSString* pageTitle = nsCocoaUtils::ToNSString(aPageTitle);
+ NSSharingService* service = [NSSharingService sharingServiceNamed:serviceName];
+
+ // Reminders fetch its data from an activity, not the share data
+ if ([[service name] isEqual:remindersServiceName]) {
+ NSUserActivity* shareActivity =
+ [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
+
+ if ([pageUrl.scheme hasPrefix:@"http"]) {
+ [shareActivity setWebpageURL:pageUrl];
+ }
+ [shareActivity setEligibleForHandoff:NO];
+ [shareActivity setTitle:pageTitle];
+ [shareActivity becomeCurrent];
+
+ // Pass ownership of shareActivity to shareDelegate, which will release the
+ // activity once sharing has completed.
+ SharingServiceDelegate* shareDelegate =
+ [[SharingServiceDelegate alloc] initWithActivity:shareActivity];
+ [shareActivity release];
+
+ [service setDelegate:shareDelegate];
+ [shareDelegate release];
+ }
+
+ // Twitter likes the the title as an additional share item
+ NSArray* toShare = [[service name] isEqual:NSSharingServiceNamePostOnTwitter]
+ ? @[ pageUrl, pageTitle ]
+ : @[ pageUrl ];
+
+ [service setSubject:pageTitle];
+ [service performWithItems:toShare];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacUserActivityUpdater.h b/widget/cocoa/nsMacUserActivityUpdater.h
new file mode 100644
index 0000000000..6870a66343
--- /dev/null
+++ b/widget/cocoa/nsMacUserActivityUpdater.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsMacUserActivityUpdater_h_
+#define nsMacUserActivityUpdater_h_
+
+#include "nsIMacUserActivityUpdater.h"
+#include "nsCocoaWindow.h"
+
+class nsMacUserActivityUpdater : public nsIMacUserActivityUpdater {
+ public:
+ nsMacUserActivityUpdater() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACUSERACTIVITYUPDATER
+
+ protected:
+ virtual ~nsMacUserActivityUpdater() {}
+ BaseWindow* GetCocoaWindow(nsIBaseWindow* aWindow);
+};
+
+#endif // nsMacUserActivityUpdater_h_
diff --git a/widget/cocoa/nsMacUserActivityUpdater.mm b/widget/cocoa/nsMacUserActivityUpdater.mm
new file mode 100644
index 0000000000..cc80e1259b
--- /dev/null
+++ b/widget/cocoa/nsMacUserActivityUpdater.mm
@@ -0,0 +1,64 @@
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsMacUserActivityUpdater.h"
+
+#include "nsCocoaUtils.h"
+#include "nsIBaseWindow.h"
+#include "gfxPlatform.h"
+
+NS_IMPL_ISUPPORTS(nsMacUserActivityUpdater, nsIMacUserActivityUpdater)
+
+NS_IMETHODIMP
+nsMacUserActivityUpdater::UpdateLocation(const nsAString& aPageUrl, const nsAString& aPageTitle,
+ nsIBaseWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if (gfxPlatform::IsHeadless()) {
+ // In headless mode, Handoff will fail since there is no Cocoa window.
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsMacUserActivityUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSURL* pageUrl = nsCocoaUtils::ToNSURL(aPageUrl);
+ if (!pageUrl ||
+ (![pageUrl.scheme isEqualToString:@"https"] && ![pageUrl.scheme isEqualToString:@"http"])) {
+ [cocoaWin.userActivity invalidate];
+ return NS_OK;
+ }
+
+ NSString* pageTitle = nsCocoaUtils::ToNSString(aPageTitle);
+ if (!pageTitle) {
+ pageTitle = pageUrl.absoluteString;
+ }
+
+ NSUserActivity* userActivity =
+ [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
+ userActivity.webpageURL = pageUrl;
+ userActivity.title = pageTitle;
+ cocoaWin.userActivity = userActivity;
+ [userActivity release];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+BaseWindow* nsMacUserActivityUpdater::GetCocoaWindow(nsIBaseWindow* aWindow) {
+ nsCOMPtr<nsIWidget> widget = nullptr;
+ aWindow->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ return nil;
+ }
+ BaseWindow* cocoaWin = (BaseWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!cocoaWin) {
+ return nil;
+ }
+ return cocoaWin;
+}
diff --git a/widget/cocoa/nsMacWebAppUtils.h b/widget/cocoa/nsMacWebAppUtils.h
new file mode 100644
index 0000000000..1e79ca92d5
--- /dev/null
+++ b/widget/cocoa/nsMacWebAppUtils.h
@@ -0,0 +1,22 @@
+/* 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/. */
+#ifndef _MAC_WEB_APP_UTILS_H_
+#define _MAC_WEB_APP_UTILS_H_
+
+#include "nsIMacWebAppUtils.h"
+
+#define NS_MACWEBAPPUTILS_CONTRACTID "@mozilla.org/widget/mac-web-app-utils;1"
+
+class nsMacWebAppUtils : public nsIMacWebAppUtils {
+ public:
+ nsMacWebAppUtils() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACWEBAPPUTILS
+
+ protected:
+ virtual ~nsMacWebAppUtils() {}
+};
+
+#endif //_MAC_WEB_APP_UTILS_H_
diff --git a/widget/cocoa/nsMacWebAppUtils.mm b/widget/cocoa/nsMacWebAppUtils.mm
new file mode 100644
index 0000000000..9098544724
--- /dev/null
+++ b/widget/cocoa/nsMacWebAppUtils.mm
@@ -0,0 +1,90 @@
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsMacWebAppUtils.h"
+#include "nsCOMPtr.h"
+#include "nsCocoaUtils.h"
+#include "nsString.h"
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+// Find the path to the app with the given bundleIdentifier, if any.
+// Note that the OS will return the path to the newest binary, if there is more than one.
+// The determination of 'newest' is complex and beyond the scope of this comment.
+
+NS_IMPL_ISUPPORTS(nsMacWebAppUtils, nsIMacWebAppUtils)
+
+NS_IMETHODIMP nsMacWebAppUtils::PathForAppWithIdentifier(const nsAString& bundleIdentifier,
+ nsAString& outPath) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ outPath.Truncate();
+
+ nsAutoreleasePool localPool;
+
+ // note that the result of this expression might be nil, meaning no matching app was found.
+ NSString* temp = [[NSWorkspace sharedWorkspace]
+ absolutePathForAppBundleWithIdentifier:
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ ((nsString)bundleIdentifier).get())
+ length:((nsString)bundleIdentifier).Length()]];
+
+ if (temp) {
+ // Copy out the resultant absolute path into outPath if non-nil.
+ nsCocoaUtils::GetStringForNSString(temp, outPath);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsMacWebAppUtils::LaunchAppWithIdentifier(const nsAString& bundleIdentifier) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoreleasePool localPool;
+
+ // Note this might return false, meaning the app wasnt launched for some reason.
+ BOOL success = [[NSWorkspace sharedWorkspace]
+ launchAppWithBundleIdentifier:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ ((nsString)bundleIdentifier)
+ .get())
+ length:((nsString)bundleIdentifier).Length()]
+ options:(NSWorkspaceLaunchOptions)0
+ additionalEventParamDescriptor:nil
+ launchIdentifier:NULL];
+
+ return success ? NS_OK : NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsMacWebAppUtils::TrashApp(const nsAString& path, nsITrashAppCallback* aCallback) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (NS_WARN_IF(!aCallback)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsITrashAppCallback> callback = aCallback;
+
+ NSString* tempString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(((nsString)path).get())
+ length:path.Length()];
+
+ [[NSWorkspace sharedWorkspace]
+ recycleURLs:[NSArray arrayWithObject:[NSURL fileURLWithPath:tempString]]
+ completionHandler:^(NSDictionary* newURLs, NSError* error) {
+ nsresult rv = (error == nil) ? NS_OK : NS_ERROR_FAILURE;
+ callback->TrashAppFinished(rv);
+ }];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h
new file mode 100644
index 0000000000..9990c59bc5
--- /dev/null
+++ b/widget/cocoa/nsMenuBarX.h
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuBarX_h_
+#define nsMenuBarX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WeakPtr.h"
+
+#include "nsISupports.h"
+#include "nsMenuParentX.h"
+#include "nsChangeObserver.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+class nsMenuBarX;
+class nsMenuGroupOwnerX;
+class nsMenuX;
+class nsIWidget;
+class nsIContent;
+
+namespace mozilla {
+namespace dom {
+class Document;
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+// ApplicationMenuDelegate is used to receive Cocoa notifications.
+@interface ApplicationMenuDelegate : NSObject <NSMenuDelegate> {
+ nsMenuBarX* mApplicationMenu; // weak ref
+}
+- (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu;
+@end
+
+// Objective-C class used to allow us to intervene with keyboard event handling.
+// We allow mouse actions to work normally.
+@interface GeckoNSMenu : NSMenu {
+}
+- (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent;
+@end
+
+// Objective-C class used as action target for menu items
+@interface NativeMenuItemTarget : NSObject {
+}
+- (IBAction)menuItemHit:(id)aSender;
+@end
+
+// Objective-C class used for menu items on the Services menu to allow Gecko
+// to override their standard behavior in order to stop key equivalents from
+// firing in certain instances.
+@interface GeckoServicesNSMenuItem : NSMenuItem {
+}
+- (id)target;
+- (SEL)action;
+- (void)_doNothing:(id)aSender;
+@end
+
+// Objective-C class used as the Services menu so that Gecko can override the
+// standard behavior of the Services menu in order to stop key equivalents
+// from firing in certain instances.
+@interface GeckoServicesNSMenu : NSMenu {
+}
+- (void)addItem:(NSMenuItem*)aNewItem;
+- (NSMenuItem*)addItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv;
+- (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex;
+- (NSMenuItem*)insertItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv
+ atIndex:(NSInteger)aIndex;
+- (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem;
+@end
+
+// Once instantiated, this object lives until its DOM node or its parent window is destroyed.
+// Do not hold references to this, they can become invalid any time the DOM node can be destroyed.
+class nsMenuBarX : public nsMenuParentX, public nsChangeObserver, public mozilla::SupportsWeakPtr {
+ public:
+ explicit nsMenuBarX(mozilla::dom::Element* aElement);
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuBarX)
+
+ static NativeMenuItemTarget* sNativeEventTarget;
+ static nsMenuBarX* sLastGeckoMenuBarPainted;
+
+ // The following content nodes have been removed from the menu system.
+ // We save them here for use in command handling.
+ RefPtr<nsIContent> mAboutItemContent;
+ RefPtr<nsIContent> mPrefItemContent;
+ RefPtr<nsIContent> mAccountItemContent;
+ RefPtr<nsIContent> mQuitItemContent;
+
+ // nsChangeObserver
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuParentX
+ nsMenuBarX* AsMenuBar() override { return this; }
+
+ // nsMenuBarX
+ uint32_t GetMenuCount();
+ bool MenuContainsAppMenu();
+ nsMenuX* GetMenuAt(uint32_t aIndex);
+ nsMenuX* GetXULHelpMenu();
+ void SetSystemHelpMenu();
+ nsresult Paint();
+ void ForceUpdateNativeMenuAt(const nsAString& aIndexString);
+ void ForceNativeMenuReload(); // used for testing
+ static void ResetNativeApplicationMenu();
+ void SetNeedsRebuild();
+ void ApplicationMenuOpened();
+ bool PerformKeyEquivalent(NSEvent* aEvent);
+ GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
+
+ // nsMenuParentX
+ void MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) override;
+
+ protected:
+ virtual ~nsMenuBarX();
+
+ void ConstructNativeMenus();
+ void ConstructFallbackNativeMenus();
+ void InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex);
+ void RemoveMenuAtIndex(uint32_t aIndex);
+ RefPtr<mozilla::dom::Element> HideItem(mozilla::dom::Document* aDocument, const nsAString& aID);
+ void AquifyMenuBar();
+ NSMenuItem* CreateNativeAppMenuItem(nsMenuX* aMenu, const nsAString& aNodeID, SEL aAction,
+ int aTag, NativeMenuItemTarget* aTarget);
+ void CreateApplicationMenu(nsMenuX* aMenu);
+
+ // Calculates the index at which aChild's NSMenuItem should be inserted into our NSMenu.
+ // The order of NSMenuItems in the NSMenu is the same as the order of nsMenuX objects in
+ // mMenuArray; there are two differences:
+ // - mMenuArray contains both visible and invisible menus, and the NSMenu only contains visible
+ // menus.
+ // - Our NSMenu may also contain an item for the app menu, whereas mMenuArray never does.
+ // So the insertion index is equal to the number of visible previous siblings of aChild in
+ // mMenuArray, plus one if the app menu is present.
+ NSInteger CalculateNativeInsertionPoint(nsMenuX* aChild);
+
+ RefPtr<nsIContent> mContent;
+ RefPtr<nsMenuGroupOwnerX> mMenuGroupOwner;
+ nsTArray<RefPtr<nsMenuX>> mMenuArray;
+ GeckoNSMenu* mNativeMenu; // root menu, representing entire menu bar
+ bool mNeedsRebuild;
+ ApplicationMenuDelegate* mApplicationMenuDelegate;
+};
+
+#endif // nsMenuBarX_h_
diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm
new file mode 100644
index 0000000000..70a17d87f8
--- /dev/null
+++ b/widget/cocoa/nsMenuBarX.mm
@@ -0,0 +1,1072 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include <objc/objc-runtime.h>
+
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+#include "nsChildView.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsGkAtoms.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+#include "nsTouchBarNativeAPIDefines.h"
+
+#include "nsIContent.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+#include "nsIAppStartup.h"
+#include "nsIStringBundle.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla;
+using mozilla::dom::Element;
+
+NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
+nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+NSMenu* sApplicationMenu = nil;
+BOOL sApplicationMenuIsFallback = NO;
+BOOL gSomeMenuBarPainted = NO;
+
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
+// We keep references to the first quit and pref item content nodes we find, which
+// will be from the hidden window. We use these when the document for the current
+// window does not have a quit or pref item. We don't need strong refs here because
+// these items are always strong ref'd by their owning menu bar (instance variable).
+static nsIContent* sAboutItemContent = nullptr;
+static nsIContent* sPrefItemContent = nullptr;
+static nsIContent* sAccountItemContent = nullptr;
+static nsIContent* sQuitItemContent = nullptr;
+
+//
+// ApplicationMenuDelegate Objective-C class
+//
+
+@implementation ApplicationMenuDelegate
+
+- (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu {
+ if ((self = [super init])) {
+ mApplicationMenu = aApplicationMenu;
+ }
+ return self;
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ mApplicationMenu->ApplicationMenuOpened();
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+}
+
+@end
+
+nsMenuBarX::nsMenuBarX(mozilla::dom::Element* aElement)
+ : mNeedsRebuild(false), mApplicationMenuDelegate(nil) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, this);
+ mMenuGroupOwner->RegisterForLocaleChanges();
+ mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
+
+ mContent = aElement;
+
+ if (mContent) {
+ AquifyMenuBar();
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+ ConstructNativeMenus();
+ } else {
+ ConstructFallbackNativeMenus();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuBarX::~nsMenuBarX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (nsMenuBarX::sLastGeckoMenuBarPainted == this) {
+ nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+ }
+
+ // the quit/pref items of a random window might have been used if there was no
+ // hidden window, thus we need to invalidate the weak references.
+ if (sAboutItemContent == mAboutItemContent) {
+ sAboutItemContent = nullptr;
+ }
+ if (sQuitItemContent == mQuitItemContent) {
+ sQuitItemContent = nullptr;
+ }
+ if (sPrefItemContent == mPrefItemContent) {
+ sPrefItemContent = nullptr;
+ }
+ if (sAccountItemContent == mAccountItemContent) {
+ sAccountItemContent = nullptr;
+ }
+
+ mMenuGroupOwner->UnregisterForLocaleChanges();
+
+ // make sure we unregister ourselves as a content observer
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+
+ for (nsMenuX* menu : mMenuArray) {
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+ }
+
+ if (mApplicationMenuDelegate) {
+ [mApplicationMenuDelegate release];
+ }
+
+ [mNativeMenu release];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ConstructNativeMenus() {
+ for (nsIContent* menuContent = mContent->GetFirstChild(); menuContent;
+ menuContent = menuContent->GetNextSibling()) {
+ if (menuContent->IsXULElement(nsGkAtoms::menu)) {
+ InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
+ GetMenuCount());
+ }
+ }
+}
+
+void nsMenuBarX::ConstructFallbackNativeMenus() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (sApplicationMenu) {
+ // Menu has already been built.
+ return;
+ }
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/fallbackMenubar.properties",
+ getter_AddRefs(stringBundle));
+
+ if (!stringBundle) {
+ return;
+ }
+
+ nsAutoString labelUTF16;
+ nsAutoString keyUTF16;
+
+ const char* labelProp = "quitMenuitem.label";
+ const char* keyProp = "quitMenuitem.key";
+
+ stringBundle->GetStringFromName(labelProp, labelUTF16);
+ stringBundle->GetStringFromName(keyProp, keyUTF16);
+
+ NSString* labelStr = [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
+ NSString* keyStr = [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
+ sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+ NSMenuItem* quitMenuItem = [[[NSMenuItem alloc] initWithTitle:labelStr
+ action:@selector(menuItemHit:)
+ keyEquivalent:keyStr] autorelease];
+ quitMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ quitMenuItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:quitMenuItem];
+ sApplicationMenuIsFallback = YES;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+uint32_t nsMenuBarX::GetMenuCount() { return mMenuArray.Length(); }
+
+bool nsMenuBarX::MenuContainsAppMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ return (mNativeMenu.numberOfItems > 0 && [mNativeMenu itemAtIndex:0].submenu == sApplicationMenu);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // If we've only yet created a fallback global Application menu (using
+ // ContructFallbackNativeMenus()), destroy it before recreating it properly.
+ if (sApplicationMenu && sApplicationMenuIsFallback) {
+ ResetNativeApplicationMenu();
+ }
+ // If we haven't created a global Application menu yet, do it.
+ if (!sApplicationMenu) {
+ CreateApplicationMenu(aMenu.get());
+
+ // Hook the new Application menu up to the menu bar.
+ NSMenu* mainMenu = NSApp.mainMenu;
+ NS_ASSERTION(mainMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+ [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
+ }
+
+ // add menu to array that owns our menus
+ mMenuArray.InsertElementAt(aIndex, aMenu);
+
+ // hook up submenus
+ RefPtr<nsIContent> menuContent = aMenu->Content();
+ if (menuContent->GetChildCount() > 0 && !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
+ MenuChildChangedVisibility(MenuChild(aMenu), true);
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Attempting submenu removal with bad index!");
+ return;
+ }
+
+ RefPtr<nsMenuX> menu = mMenuArray[aIndex];
+ mMenuArray.RemoveElementAt(aIndex);
+
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+
+ // Our native menu and our internal menu object array might be out of sync.
+ // This happens, for example, when a submenu is hidden. Because of this we
+ // should not assume that a native submenu is hooked up.
+ NSMenuItem* nativeMenuItem = menu->NativeNSMenuItem();
+ int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
+ if (nativeMenuItemIndex != -1) {
+ [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ObserveAttributeChanged(mozilla::dom::Document* aDocument, nsIContent* aContent,
+ nsAtom* aAttribute) {}
+
+void nsMenuBarX::ObserveContentRemoved(mozilla::dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild, nsIContent* aPreviousSibling) {
+ nsINode* parent = NODE_FROM(aContainer, aDocument);
+ MOZ_ASSERT(parent);
+ const Maybe<uint32_t> index = parent->ComputeIndexOf(aPreviousSibling);
+ MOZ_ASSERT(*index != UINT32_MAX);
+ RemoveMenuAtIndex(index.isSome() ? *index + 1u : 0u);
+}
+
+void nsMenuBarX::ObserveContentInserted(mozilla::dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild) {
+ InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aChild),
+ aContainer->ComputeIndexOf(aChild).valueOr(UINT32_MAX));
+}
+
+void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
+ unsigned int indexCount = indexes.count;
+ if (indexCount == 0) {
+ return;
+ }
+
+ RefPtr<nsMenuX> currentMenu = nullptr;
+ int targetIndex = [[indexes objectAtIndex:0] intValue];
+ int visible = 0;
+ uint32_t length = mMenuArray.Length();
+ // first find a menu in the menu bar
+ for (unsigned int i = 0; i < length; i++) {
+ RefPtr<nsMenuX> menu = mMenuArray[i];
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
+ visible++;
+ if (visible == (targetIndex + 1)) {
+ currentMenu = std::move(menu);
+ break;
+ }
+ }
+ }
+
+ if (!currentMenu) {
+ return;
+ }
+
+ // fake open/close to cause lazy update to happen so submenus populate
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ // now find the correct submenu
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ targetIndex = [[indexes objectAtIndex:i] intValue];
+ visible = 0;
+ length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+ break;
+ }
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// Calling this forces a full reload of the menu system, reloading all native
+// menus and their items.
+// Without this testing is hard because changes to the DOM affect the native
+// menu system lazily.
+void nsMenuBarX::ForceNativeMenuReload() {
+ // tear down everything
+ while (GetMenuCount() > 0) {
+ RemoveMenuAtIndex(0);
+ }
+
+ // construct everything
+ ConstructNativeMenus();
+}
+
+nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex) {
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Requesting menu at invalid index!");
+ return nullptr;
+ }
+ return mMenuArray[aIndex].get();
+}
+
+nsMenuX* nsMenuBarX::GetXULHelpMenu() {
+ // The Help menu is usually (always?) the last one, so we start there and
+ // count back.
+ for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
+ nsMenuX* aMenu = GetMenuAt(i);
+ if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
+ return aMenu;
+ }
+ }
+ return nil;
+}
+
+// On SnowLeopard and later we must tell the OS which is our Help menu.
+// Otherwise it will only add Spotlight for Help (the Search item) to our
+// Help menu if its label/title is "Help" -- i.e. if the menu is in English.
+// This resolves bugs 489196 and 539317.
+void nsMenuBarX::SetSystemHelpMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ nsMenuX* xulHelpMenu = GetXULHelpMenu();
+ if (xulHelpMenu) {
+ NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
+ if (helpMenu) {
+ NSApp.helpMenu = helpMenu;
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsresult nsMenuBarX::Paint() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Don't try to optimize anything in this painting by checking
+ // sLastGeckoMenuBarPainted because the menubar can be manipulated by
+ // native dialogs and sheet code and other things besides this paint method.
+
+ // We have to keep the same menu item for the Application menu so we keep
+ // passing it along.
+ NSMenu* outgoingMenu = NSApp.mainMenu;
+ NS_ASSERTION(outgoingMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
+ [outgoingMenu removeItemAtIndex:0];
+ [mNativeMenu insertItem:appMenuItem atIndex:0];
+ [appMenuItem release];
+
+ // Set menu bar and event target.
+ NSApp.mainMenu = mNativeMenu;
+ SetSystemHelpMenu();
+ nsMenuBarX::sLastGeckoMenuBarPainted = this;
+
+ gSomeMenuBarPainted = YES;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+/* static */
+void nsMenuBarX::ResetNativeApplicationMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ [sApplicationMenu removeAllItems];
+ [sApplicationMenu release];
+ sApplicationMenu = nil;
+ sApplicationMenuIsFallback = NO;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::SetNeedsRebuild() { mNeedsRebuild = true; }
+
+void nsMenuBarX::ApplicationMenuOpened() {
+ if (mNeedsRebuild) {
+ if (!mMenuArray.IsEmpty()) {
+ ResetNativeApplicationMenu();
+ CreateApplicationMenu(mMenuArray[0].get());
+ }
+ mNeedsRebuild = false;
+ }
+}
+
+bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
+ return [mNativeMenu performSuperKeyEquivalent:aEvent];
+}
+
+void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) {
+ MOZ_RELEASE_ASSERT(aChild.is<RefPtr<nsMenuX>>(), "nsMenuBarX only has nsMenuX children");
+ const RefPtr<nsMenuX>& child = aChild.as<RefPtr<nsMenuX>>();
+ NSMenuItem* item = child->NativeNSMenuItem();
+ if (aIsVisible) {
+ NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
+ [mNativeMenu insertItem:child->NativeNSMenuItem() atIndex:insertionPoint];
+ } else if ([mNativeMenu indexOfItem:item] != -1) {
+ [mNativeMenu removeItem:item];
+ }
+}
+
+NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
+ NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
+ for (auto& currMenu : mMenuArray) {
+ if (currMenu == aChild) {
+ return insertionPoint;
+ }
+ // Only count items that are inside a menu.
+ // XXXmstange Not sure what would cause free-standing items. Maybe for collapsed/hidden menus?
+ // In that case, an nsMenuX::IsVisible() method would be better.
+ if (currMenu->NativeNSMenuItem().menu) {
+ insertionPoint++;
+ }
+ }
+ return insertionPoint;
+}
+
+// Hide the item in the menu by setting the 'hidden' attribute. Returns it so
+// the caller can hang onto it if they so choose.
+RefPtr<Element> nsMenuBarX::HideItem(mozilla::dom::Document* aDocument, const nsAString& aID) {
+ RefPtr<Element> menuElement = aDocument->GetElementById(aID);
+ if (menuElement) {
+ menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns, false);
+ }
+ return menuElement;
+}
+
+// Do what is necessary to conform to the Aqua guidelines for menus.
+void nsMenuBarX::AquifyMenuBar() {
+ RefPtr<mozilla::dom::Document> domDoc = mContent->GetComposedDoc();
+ if (domDoc) {
+ // remove the "About..." item and its separator
+ HideItem(domDoc, u"aboutSeparator"_ns);
+ mAboutItemContent = HideItem(domDoc, u"aboutName"_ns);
+ if (!sAboutItemContent) {
+ sAboutItemContent = mAboutItemContent;
+ }
+
+ // remove quit item and its separator
+ HideItem(domDoc, u"menu_FileQuitSeparator"_ns);
+ mQuitItemContent = HideItem(domDoc, u"menu_FileQuitItem"_ns);
+ if (!sQuitItemContent) {
+ sQuitItemContent = mQuitItemContent;
+ }
+
+ // remove prefs item and its separator, but save off the pref content node
+ // so we can invoke its command later.
+ HideItem(domDoc, u"menu_PrefsSeparator"_ns);
+ mPrefItemContent = HideItem(domDoc, u"menu_preferences"_ns);
+ if (!sPrefItemContent) {
+ sPrefItemContent = mPrefItemContent;
+ }
+
+ // remove Account Settings item.
+ mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
+ if (!sAccountItemContent) {
+ sAccountItemContent = mAccountItemContent;
+ }
+
+ // hide items that we use for the Application menu
+ HideItem(domDoc, u"menu_mac_services"_ns);
+ HideItem(domDoc, u"menu_mac_hide_app"_ns);
+ HideItem(domDoc, u"menu_mac_hide_others"_ns);
+ HideItem(domDoc, u"menu_mac_show_all"_ns);
+ HideItem(domDoc, u"menu_mac_touch_bar"_ns);
+ }
+}
+
+// for creating menu items destined for the Application menu
+NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* aMenu, const nsAString& aNodeID,
+ SEL aAction, int aTag,
+ NativeMenuItemTarget* aTarget) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ RefPtr<mozilla::dom::Document> doc = aMenu->Content()->GetUncomposedDoc();
+ if (!doc) {
+ return nil;
+ }
+
+ RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
+ if (!menuItem) {
+ return nil;
+ }
+
+ // Check collapsed rather than hidden since the app menu items are always
+ // hidden in AquifyMenuBar.
+ if (menuItem->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed, nsGkAtoms::_true,
+ eCaseMatters)) {
+ return nil;
+ }
+
+ // Get information from the gecko menu item
+ nsAutoString label;
+ nsAutoString modifiers;
+ nsAutoString key;
+ menuItem->GetAttr(nsGkAtoms::label, label);
+ menuItem->GetAttr(nsGkAtoms::modifiers, modifiers);
+ menuItem->GetAttr(nsGkAtoms::key, key);
+
+ // Get more information about the key equivalent. Start by
+ // finding the key node we need.
+ NSString* keyEquiv = nil;
+ unsigned int macKeyModifiers = 0;
+ if (!key.IsEmpty()) {
+ RefPtr<Element> keyElement = doc->GetElementById(key);
+ if (keyElement) {
+ // first grab the key equivalent character
+ nsAutoString keyChar(u" "_ns);
+ keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
+ if (!keyChar.EqualsLiteral(" ")) {
+ keyEquiv = [[NSString stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
+ length:keyChar.Length()] lowercaseString];
+ }
+ // now grab the key equivalent modifiers
+ nsAutoString modifiersStr;
+ keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
+ uint8_t geckoModifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+ macKeyModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
+ }
+ }
+ // get the label into NSString-form
+ NSString* labelString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
+ length:label.Length()];
+
+ if (!labelString) {
+ labelString = @"";
+ }
+ if (!keyEquiv) {
+ keyEquiv = @"";
+ }
+
+ // put together the actual NSMenuItem
+ NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString
+ action:aAction
+ keyEquivalent:keyEquiv];
+
+ newMenuItem.tag = aTag;
+ newMenuItem.target = aTarget;
+ newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
+ newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+
+ return newMenuItem;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// build the Application menu shared by all menu bars
+void nsMenuBarX::CreateApplicationMenu(nsMenuX* aMenu) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // At this point, the application menu is the application menu from
+ // the nib in cocoa widgets. We do not have a way to create an application
+ // menu manually, so we grab the one from the nib and use that.
+ sApplicationMenu = [[NSApp.mainMenu itemAtIndex:0].submenu retain];
+
+ /*
+ We support the following menu items here:
+
+ Menu Item DOM Node ID Notes
+
+ ========================
+ = About This App = <- aboutName
+ ========================
+ = Preferences... = <- menu_preferences
+ = Account Settings = <- menu_accountmgr Only on Thunderbird
+ ========================
+ = Services > = <- menu_mac_services <- (do not define key equivalent)
+ ========================
+ = Hide App = <- menu_mac_hide_app
+ = Hide Others = <- menu_mac_hide_others
+ = Show All = <- menu_mac_show_all
+ ========================
+ = Customize Touch Bar… = <- menu_mac_touch_bar
+ ========================
+ = Quit = <- menu_FileQuitItem
+ ========================
+
+ If any of them are ommitted from the application's DOM, we just don't add
+ them. We always add a "Quit" item, but if an app developer does not provide a
+ DOM node with the right ID for the Quit item, we add it in English. App
+ developers need only add each node with a label and a key equivalent (if they
+ want one). Other attributes are optional. Like so:
+
+ <menuitem id="menu_preferences"
+ label="&preferencesCmdMac.label;"
+ key="open_prefs_key"/>
+
+ We need to use this system for localization purposes, until we have a better way
+ to define the Application menu to be used on Mac OS X.
+ */
+
+ if (sApplicationMenu) {
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+
+ // This code reads attributes we are going to care about from the DOM elements
+
+ NSMenuItem* itemBeingAdded = nil;
+ BOOL addAboutSeparator = FALSE;
+ BOOL addPrefsSeparator = FALSE;
+
+ // Add the About menu item
+ itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"aboutName"_ns, @selector(menuItemHit:),
+ eCommand_ID_About, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addAboutSeparator = TRUE;
+ }
+
+ // Add separator if either the About item or software update item exists
+ if (addAboutSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add the Preferences menu item
+ itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_preferences"_ns, @selector(menuItemHit:),
+ eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addPrefsSeparator = TRUE;
+ }
+
+ // Add the Account Settings menu item. This is Thunderbird only
+ itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_accountmgr"_ns, @selector(menuItemHit:),
+ eCommand_ID_Account, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add separator after Preferences menu
+ if (addPrefsSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add Services menu item
+ itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_mac_services"_ns, nil, 0, nil);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+
+ // set this menu item up as the Mac OS X Services menu
+ NSMenu* servicesMenu = [[GeckoServicesNSMenu alloc] initWithTitle:@""];
+ itemBeingAdded.submenu = servicesMenu;
+ NSApp.servicesMenu = servicesMenu;
+
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ // Add separator after Services menu
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addHideShowSeparator = FALSE;
+
+ // Add menu item to hide this application
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_hide_app"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to hide other applications
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_hide_others"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to show all applications
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_show_all"_ns, @selector(menuItemHit:),
+ eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add a separator after the hide/show menus if at least one exists
+ if (addHideShowSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addTouchBarSeparator = NO;
+
+ // Add Touch Bar customization menu item.
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_touch_bar"_ns, @selector(menuItemHit:),
+ eCommand_ID_TouchBar, nsMenuBarX::sNativeEventTarget);
+
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ // We hide the menu item on Macs that don't have a Touch Bar.
+ if (!sTouchBarIsInitialized) {
+ [itemBeingAdded setHidden:YES];
+ } else {
+ addTouchBarSeparator = YES;
+ }
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add a separator after the Touch Bar menu item if it exists
+ if (addTouchBarSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add quit menu item
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_FileQuitItem"_ns, @selector(menuItemHit:),
+ eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ } else {
+ // the current application does not have a DOM node for "Quit". Add one
+ // anyway, in English.
+ NSMenuItem* defaultQuitItem = [[[NSMenuItem alloc] initWithTitle:@"Quit"
+ action:@selector(menuItemHit:)
+ keyEquivalent:@"q"] autorelease];
+ defaultQuitItem.target = nsMenuBarX::sNativeEventTarget;
+ defaultQuitItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:defaultQuitItem];
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+//
+// Objective-C class used to allow us to have keyboard commands
+// look like they are doing something but actually do nothing.
+// We allow mouse actions to work normally.
+//
+
+// Controls whether or not native menu items should invoke their commands.
+static BOOL gMenuItemsExecuteCommands = YES;
+
+@implementation GeckoNSMenu
+
+// Keyboard commands should not cause menu items to invoke their
+// commands when there is a key window because we'd rather send
+// the keyboard command to the window. We still have the menus
+// go through the mechanics so they'll give the proper visual
+// feedback.
+- (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
+ // We've noticed that Mac OS X expects this check in subclasses before
+ // calling NSMenu's "performKeyEquivalent:".
+ //
+ // There is no case in which we'd need to do anything or return YES
+ // when we have no items so we can just do this check first.
+ if (self.numberOfItems <= 0) {
+ return NO;
+ }
+
+ NSWindow* keyWindow = NSApp.keyWindow;
+
+ // If there is no key window then just behave normally. This
+ // probably means that this menu is associated with Gecko's
+ // hidden window.
+ if (!keyWindow) {
+ return [super performKeyEquivalent:aEvent];
+ }
+
+ NSResponder* firstResponder = keyWindow.firstResponder;
+
+ gMenuItemsExecuteCommands = NO;
+ [super performKeyEquivalent:aEvent];
+ gMenuItemsExecuteCommands = YES; // return to default
+
+ // Return YES if we invoked a command and there is now no key window or we changed
+ // the first responder. In this case we do not want to propagate the event because
+ // we don't want it handled again.
+ if (!NSApp.keyWindow || NSApp.keyWindow.firstResponder != firstResponder) {
+ return YES;
+ }
+
+ // Return NO so that we can handle the event via NSView's "keyDown:".
+ return NO;
+}
+
+- (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
+ return [super performKeyEquivalent:aEvent];
+}
+
+@end
+
+//
+// Objective-C class used as action target for menu items
+//
+
+@implementation NativeMenuItemTarget
+
+// called when some menu item in this menu gets hit
+- (IBAction)menuItemHit:(id)aSender {
+ if (!gMenuItemsExecuteCommands) {
+ return;
+ }
+
+ if (![aSender isKindOfClass:[NSMenuItem class]]) {
+ return;
+ }
+
+ NSMenuItem* nativeMenuItem = (NSMenuItem*)aSender;
+ NSInteger tag = nativeMenuItem.tag;
+
+ nsMenuGroupOwnerX* menuGroupOwner = nullptr;
+ nsMenuBarX* menuBar = nullptr;
+ MOZMenuItemRepresentedObject* representedObject = nativeMenuItem.representedObject;
+
+ if (representedObject) {
+ menuGroupOwner = representedObject.menuGroupOwner;
+ if (!menuGroupOwner) {
+ return;
+ }
+ menuBar = menuGroupOwner->GetMenuBar();
+ }
+
+ // Notify containing menu about the fact that a menu item will be activated.
+ NSMenu* menu = nativeMenuItem.menu;
+ if ([menu.delegate isKindOfClass:[MenuDelegate class]]) {
+ [(MenuDelegate*)menu.delegate menu:menu willActivateItem:nativeMenuItem];
+ }
+
+ // Get the modifier flags and button for this menu item activation. The menu system does not pass
+ // an NSEvent to our action selector, but we can query the current NSEvent instead. The current
+ // NSEvent can be a key event or a mouseup event, depending on how the menu item is activated.
+ NSEventModifierFlags modifierFlags = NSApp.currentEvent ? NSApp.currentEvent.modifierFlags : 0;
+ mozilla::MouseButton button = NSApp.currentEvent
+ ? nsCocoaUtils::ButtonForEvent(NSApp.currentEvent)
+ : mozilla::MouseButton::ePrimary;
+
+ // Do special processing if this is for an app-global command.
+ if (tag == eCommand_ID_About) {
+ nsIContent* mostSpecificContent = sAboutItemContent;
+ if (menuBar && menuBar->mAboutItemContent) {
+ mostSpecificContent = menuBar->mAboutItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Prefs) {
+ nsIContent* mostSpecificContent = sPrefItemContent;
+ if (menuBar && menuBar->mPrefItemContent) {
+ mostSpecificContent = menuBar->mPrefItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Account) {
+ nsIContent* mostSpecificContent = sAccountItemContent;
+ if (menuBar && menuBar->mAccountItemContent) {
+ mostSpecificContent = menuBar->mAccountItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_HideApp) {
+ [NSApp hide:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_HideOthers) {
+ [NSApp hideOtherApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_ShowAll) {
+ [NSApp unhideAllApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_TouchBar) {
+ [NSApp toggleTouchBarCustomizationPalette:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_Quit) {
+ nsIContent* mostSpecificContent = sQuitItemContent;
+ if (menuBar && menuBar->mQuitItemContent) {
+ mostSpecificContent = menuBar->mQuitItemContent;
+ }
+ // If we have some content for quit we execute it. Otherwise we send a native app terminate
+ // message. If you want to stop a quit from happening, provide quit content and return
+ // the event as unhandled.
+ if (mostSpecificContent) {
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ } else {
+ nsCOMPtr<nsIAppStartup> appStartup = mozilla::components::AppStartup::Service();
+ if (appStartup) {
+ bool userAllowedQuit = true;
+ appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
+ }
+ }
+ return;
+ }
+
+ // given the commandID, look it up in our hashtable and dispatch to
+ // that menu item.
+ if (menuGroupOwner) {
+ if (RefPtr<nsMenuItemX> menuItem =
+ menuGroupOwner->GetMenuItemForCommandID(static_cast<uint32_t>(tag))) {
+ if (nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest) {
+ menuItem->DoCommand(modifierFlags, button);
+ } else if (RefPtr<nsMenuX> menu = menuItem->ParentMenu()) {
+ menu->ActivateItemAfterClosing(std::move(menuItem), modifierFlags, button);
+ }
+ }
+ }
+}
+
+@end
+
+// Objective-C class used for menu items on the Services menu to allow Gecko
+// to override their standard behavior in order to stop key equivalents from
+// firing in certain instances. When gMenuItemsExecuteCommands is NO, we return
+// a dummy target and action instead of the actual target and action.
+
+@implementation GeckoServicesNSMenuItem
+
+- (id)target {
+ id realTarget = super.target;
+ if (gMenuItemsExecuteCommands) {
+ return realTarget;
+ }
+ return realTarget ? self : nil;
+}
+
+- (SEL)action {
+ SEL realAction = super.action;
+ if (gMenuItemsExecuteCommands) {
+ return realAction;
+ }
+ return realAction ? @selector(_doNothing:) : nullptr;
+}
+
+- (void)_doNothing:(id)aSender {
+}
+
+@end
+
+// Objective-C class used as the Services menu so that Gecko can override the
+// standard behavior of the Services menu in order to stop key equivalents
+// from firing in certain instances.
+
+@implementation GeckoServicesNSMenu
+
+- (void)addItem:(NSMenuItem*)aNewItem {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super addItem:aNewItem];
+}
+
+- (NSMenuItem*)addItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv {
+ NSMenuItem* newItem = [super addItemWithTitle:aString action:aSelector keyEquivalent:aKeyEquiv];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super insertItem:aNewItem atIndex:aIndex];
+}
+
+- (NSMenuItem*)insertItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv
+ atIndex:(NSInteger)aIndex {
+ NSMenuItem* newItem = [super insertItemWithTitle:aString
+ action:aSelector
+ keyEquivalent:aKeyEquiv
+ atIndex:aIndex];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
+ if ([aMenuItem class] == [NSMenuItem class]) {
+ object_setClass(aMenuItem, [GeckoServicesNSMenuItem class]);
+ }
+}
+
+@end
diff --git a/widget/cocoa/nsMenuGroupOwnerX.h b/widget/cocoa/nsMenuGroupOwnerX.h
new file mode 100644
index 0000000000..a9060a6029
--- /dev/null
+++ b/widget/cocoa/nsMenuGroupOwnerX.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuGroupOwnerX_h_
+#define nsMenuGroupOwnerX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/WeakPtr.h"
+
+#include "nsStubMutationObserver.h"
+#include "nsHashKeys.h"
+#include "nsIObserver.h"
+#include "nsMenuBarX.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+
+class nsMenuItemX;
+class nsChangeObserver;
+class nsIWidget;
+class nsIContent;
+
+@class MOZMenuItemRepresentedObject;
+
+// Fixed command IDs that work even without a JS listener, for our fallback menu bar.
+// Dynamic command IDs start counting from eCommand_ID_Last.
+enum {
+ eCommand_ID_About = 1,
+ eCommand_ID_Prefs = 2,
+ eCommand_ID_Quit = 3,
+ eCommand_ID_HideApp = 4,
+ eCommand_ID_HideOthers = 5,
+ eCommand_ID_ShowAll = 6,
+ eCommand_ID_Update = 7,
+ eCommand_ID_TouchBar = 8,
+ eCommand_ID_Account = 9,
+ eCommand_ID_Last = 10
+};
+
+// The menu group owner observes DOM mutations, notifies registered nsChangeObservers, and manages
+// command registration.
+// There is one owner per menubar, and one per standalone native menu.
+class nsMenuGroupOwnerX : public nsMultiMutationObserver, public nsIObserver {
+ public:
+ // Both parameters can be null.
+ nsMenuGroupOwnerX(mozilla::dom::Element* aElement, nsMenuBarX* aMenuBarIfMenuBar);
+
+ void RegisterForContentChanges(nsIContent* aContent, nsChangeObserver* aMenuObject);
+ void UnregisterForContentChanges(nsIContent* aContent);
+ uint32_t RegisterForCommand(nsMenuItemX* aMenuItem);
+ void UnregisterCommand(uint32_t aCommandID);
+ nsMenuItemX* GetMenuItemForCommandID(uint32_t aCommandID);
+
+ void RegisterForLocaleChanges();
+ void UnregisterForLocaleChanges();
+
+ // The representedObject that's used for all menu items under this menu group owner.
+ MOZMenuItemRepresentedObject* GetRepresentedObject() { return mRepresentedObject; }
+
+ // If this is the group owner for a menubar, return the menubar, otherwise nullptr.
+ nsMenuBarX* GetMenuBar() { return mMenuBar.get(); }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMUTATIONOBSERVER
+
+ protected:
+ virtual ~nsMenuGroupOwnerX();
+
+ nsChangeObserver* LookupContentChangeObserver(nsIContent* aContent);
+
+ RefPtr<nsIContent> mContent;
+
+ // Unique command id (per menu-bar) to give to next item that asks.
+ uint32_t mCurrentCommandID = eCommand_ID_Last;
+
+ // stores observers for content change notification
+ nsTHashMap<nsPtrHashKey<nsIContent>, nsChangeObserver*> mContentToObserverTable;
+
+ // stores mapping of command IDs to menu objects
+ nsTHashMap<nsUint32HashKey, nsMenuItemX*> mCommandToMenuObjectTable;
+
+ MOZMenuItemRepresentedObject* mRepresentedObject = nil; // [strong]
+ mozilla::WeakPtr<nsMenuBarX> mMenuBar;
+};
+
+@interface MOZMenuItemRepresentedObject : NSObject
+- (id)initWithMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner;
+- (void)setMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner;
+- (nsMenuGroupOwnerX*)menuGroupOwner;
+@end
+
+#endif // nsMenuGroupOwner_h_
diff --git a/widget/cocoa/nsMenuGroupOwnerX.mm b/widget/cocoa/nsMenuGroupOwnerX.mm
new file mode 100644
index 0000000000..e7a0d2cf87
--- /dev/null
+++ b/widget/cocoa/nsMenuGroupOwnerX.mm
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaWindow.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/dom/Element.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+
+#include "nsINode.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsMenuGroupOwnerX, nsIObserver, nsIMutationObserver)
+
+nsMenuGroupOwnerX::nsMenuGroupOwnerX(mozilla::dom::Element* aElement, nsMenuBarX* aMenuBarIfMenuBar)
+ : mContent(aElement), mMenuBar(aMenuBarIfMenuBar) {
+ mRepresentedObject = [[MOZMenuItemRepresentedObject alloc] initWithMenuGroupOwner:this];
+}
+
+nsMenuGroupOwnerX::~nsMenuGroupOwnerX() {
+ MOZ_ASSERT(mContentToObserverTable.Count() == 0, "have outstanding mutation observers!\n");
+ [mRepresentedObject setMenuGroupOwner:nullptr];
+ [mRepresentedObject release];
+}
+
+//
+// nsIMutationObserver
+//
+
+void nsMenuGroupOwnerX::CharacterDataWillChange(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void nsMenuGroupOwnerX::CharacterDataChanged(nsIContent* aContent, const CharacterDataChangeInfo&) {
+}
+
+void nsMenuGroupOwnerX::ContentAppended(nsIContent* aFirstNewContent) {
+ for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
+ ContentInserted(cur);
+ }
+}
+
+void nsMenuGroupOwnerX::NodeWillBeDestroyed(nsINode* aNode) {}
+
+void nsMenuGroupOwnerX::AttributeWillChange(dom::Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {}
+
+void nsMenuGroupOwnerX::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(aElement);
+ if (obs) {
+ obs->ObserveAttributeChanged(aElement->OwnerDoc(), aElement, aAttribute);
+ }
+}
+
+void nsMenuGroupOwnerX::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) {
+ nsIContent* container = aChild->GetParent();
+ if (!container) {
+ return;
+ }
+
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(container);
+ if (obs) {
+ obs->ObserveContentRemoved(aChild->OwnerDoc(), container, aChild, aPreviousSibling);
+ } else if (container != mContent) {
+ // We do a lookup on the parent container in case things were removed
+ // under a "menupopup" item. That is basically a wrapper for the contents
+ // of a "menu" node.
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ obs = LookupContentChangeObserver(parent);
+ if (obs) {
+ obs->ObserveContentRemoved(aChild->OwnerDoc(), container, aChild, aPreviousSibling);
+ }
+ }
+ }
+}
+
+void nsMenuGroupOwnerX::ContentInserted(nsIContent* aChild) {
+ nsIContent* container = aChild->GetParent();
+ if (!container) {
+ return;
+ }
+
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(container);
+ if (obs) {
+ obs->ObserveContentInserted(aChild->OwnerDoc(), container, aChild);
+ } else if (container != mContent) {
+ // We do a lookup on the parent container in case things were removed
+ // under a "menupopup" item. That is basically a wrapper for the contents
+ // of a "menu" node.
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ obs = LookupContentChangeObserver(parent);
+ if (obs) {
+ obs->ObserveContentInserted(aChild->OwnerDoc(), container, aChild);
+ }
+ }
+ }
+}
+
+void nsMenuGroupOwnerX::ParentChainChanged(nsIContent* aContent) {}
+
+void nsMenuGroupOwnerX::ARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement,
+ nsAtom* aAttribute, int32_t aModType) {}
+
+void nsMenuGroupOwnerX::ARIAAttributeDefaultChanged(mozilla::dom::Element* aElement,
+ nsAtom* aAttribute, int32_t aModType) {}
+
+// For change management, we don't use a |nsSupportsHashtable| because
+// we know that the lifetime of all these items is bounded by the
+// lifetime of the menubar. No need to add any more strong refs to the
+// picture because the containment hierarchy already uses strong refs.
+void nsMenuGroupOwnerX::RegisterForContentChanges(nsIContent* aContent,
+ nsChangeObserver* aMenuObject) {
+ if (!mContentToObserverTable.Contains(aContent)) {
+ aContent->AddMutationObserver(this);
+ }
+ mContentToObserverTable.InsertOrUpdate(aContent, aMenuObject);
+}
+
+void nsMenuGroupOwnerX::UnregisterForContentChanges(nsIContent* aContent) {
+ if (mContentToObserverTable.Contains(aContent)) {
+ aContent->RemoveMutationObserver(this);
+ }
+ mContentToObserverTable.Remove(aContent);
+}
+
+void nsMenuGroupOwnerX::RegisterForLocaleChanges() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "intl:app-locales-changed", false);
+ }
+}
+
+void nsMenuGroupOwnerX::UnregisterForLocaleChanges() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "intl:app-locales-changed");
+ }
+}
+
+NS_IMETHODIMP
+nsMenuGroupOwnerX::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) {
+ if (mMenuBar && !strcmp(aTopic, "intl:app-locales-changed")) {
+ // Rebuild the menu with the new locale strings.
+ mMenuBar->SetNeedsRebuild();
+ }
+ return NS_OK;
+}
+
+nsChangeObserver* nsMenuGroupOwnerX::LookupContentChangeObserver(nsIContent* aContent) {
+ nsChangeObserver* result;
+ if (mContentToObserverTable.Get(aContent, &result)) {
+ return result;
+ }
+ return nullptr;
+}
+
+// Given a menu item, creates a unique 4-character command ID and
+// maps it to the item. Returns the id for use by the client.
+uint32_t nsMenuGroupOwnerX::RegisterForCommand(nsMenuItemX* aMenuItem) {
+ // no real need to check for uniqueness. We always start afresh with each
+ // window at 1. Even if we did get close to the reserved Apple command id's,
+ // those don't start until at least ' ', which is integer 538976288. If
+ // we have that many menu items in one window, I think we have other
+ // problems.
+
+ // make id unique
+ ++mCurrentCommandID;
+
+ mCommandToMenuObjectTable.InsertOrUpdate(mCurrentCommandID, aMenuItem);
+
+ return mCurrentCommandID;
+}
+
+// Removes the mapping between the given 4-character command ID
+// and its associated menu item.
+void nsMenuGroupOwnerX::UnregisterCommand(uint32_t aCommandID) {
+ mCommandToMenuObjectTable.Remove(aCommandID);
+}
+
+nsMenuItemX* nsMenuGroupOwnerX::GetMenuItemForCommandID(uint32_t aCommandID) {
+ nsMenuItemX* result;
+ if (mCommandToMenuObjectTable.Get(aCommandID, &result)) {
+ return result;
+ }
+ return nullptr;
+}
+
+@implementation MOZMenuItemRepresentedObject {
+ nsMenuGroupOwnerX* mMenuGroupOwner; // weak, cleared by nsMenuGroupOwnerX's destructor
+}
+
+- (id)initWithMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner {
+ self = [super init];
+ mMenuGroupOwner = aMenuGroupOwner;
+ return self;
+}
+
+- (void)setMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner {
+ mMenuGroupOwner = aMenuGroupOwner;
+}
+
+- (nsMenuGroupOwnerX*)menuGroupOwner {
+ return mMenuGroupOwner;
+}
+
+@end
diff --git a/widget/cocoa/nsMenuItemIconX.h b/widget/cocoa/nsMenuItemIconX.h
new file mode 100644
index 0000000000..e60e920f9d
--- /dev/null
+++ b/widget/cocoa/nsMenuItemIconX.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Retrieves and displays icons in native menu items on Mac OS X.
+ */
+
+#ifndef nsMenuItemIconX_h_
+#define nsMenuItemIconX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/widget/IconLoader.h"
+#include "mozilla/WeakPtr.h"
+
+class nsIconLoaderService;
+class nsIURI;
+class nsIContent;
+class nsIPrincipal;
+class imgRequestProxy;
+class nsMenuParentX;
+class nsPresContext;
+
+namespace mozilla {
+class ComputedStyle;
+}
+
+class nsMenuItemIconX final : public mozilla::widget::IconLoader::Listener {
+ public:
+ class Listener {
+ public:
+ virtual void IconUpdated() = 0;
+ };
+
+ explicit nsMenuItemIconX(Listener* aListener);
+ ~nsMenuItemIconX();
+
+ // SetupIcon starts the icon load. Once the icon has loaded,
+ // nsMenuParentX::IconUpdated will be called. The icon image needs to be
+ // retrieved from GetIconImage(). If aContent is an icon-less menuitem,
+ // GetIconImage() will return nil. If it does have an icon, GetIconImage()
+ // will return a transparent placeholder icon during the load and the actual
+ // icon when the load is completed.
+ void SetupIcon(nsIContent* aContent);
+
+ // Implements this method for mozilla::widget::IconLoader::Listener.
+ // Called once the icon load is complete.
+ nsresult OnComplete(imgIContainer* aImage) override;
+
+ // Returns a weak reference to the icon image that is owned by this class. Can
+ // return nil.
+ NSImage* GetIconImage() const { return mIconImage; }
+
+ protected:
+ // Returns whether there should be an icon.
+ bool StartIconLoad(nsIContent* aContent);
+
+ // GetIconURI returns null if the item should not have any icon.
+ already_AddRefed<nsIURI> GetIconURI(nsIContent* aContent);
+
+ Listener* mListener; // [weak]
+ RefPtr<const mozilla::ComputedStyle> mComputedStyle;
+ mozilla::WeakPtr<nsPresContext> mPresContext;
+ NSImage* mIconImage = nil; // [strong]
+ RefPtr<mozilla::widget::IconLoader> mIconLoader;
+};
+
+#endif // nsMenuItemIconX_h_
diff --git a/widget/cocoa/nsMenuItemIconX.mm b/widget/cocoa/nsMenuItemIconX.mm
new file mode 100644
index 0000000000..db94ec910e
--- /dev/null
+++ b/widget/cocoa/nsMenuItemIconX.mm
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Retrieves and displays icons in native menu items on Mac OS X.
+ */
+
+/* exception_defines.h defines 'try' to 'if (true)' which breaks objective-c
+ exceptions and produces errors like: error: unexpected '@' in program'.
+ If we define __EXCEPTIONS exception_defines.h will avoid doing this.
+
+ See bug 666609 for more information.
+
+ We use <limits> to get the libstdc++ version. */
+#include <limits>
+#if __GLIBCXX__ <= 20070719
+# ifndef __EXCEPTIONS
+# define __EXCEPTIONS
+# endif
+#endif
+
+#include "MOZIconHelper.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsCocoaUtils.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIContentPolicy.h"
+#include "nsMenuItemX.h"
+#include "nsMenuItemIconX.h"
+#include "nsNameSpaceManager.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+using mozilla::widget::IconLoader;
+
+static const uint32_t kIconSize = 16;
+
+nsMenuItemIconX::nsMenuItemIconX(Listener* aListener) : mListener(aListener) {
+ MOZ_COUNT_CTOR(nsMenuItemIconX);
+}
+
+nsMenuItemIconX::~nsMenuItemIconX() {
+ if (mIconLoader) {
+ mIconLoader->Destroy();
+ }
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ MOZ_COUNT_DTOR(nsMenuItemIconX);
+}
+
+void nsMenuItemIconX::SetupIcon(nsIContent* aContent) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ bool shouldHaveIcon = StartIconLoad(aContent);
+ if (!shouldHaveIcon) {
+ // There is no icon for this menu item, as an error occurred while loading it.
+ // An icon might have been set earlier or the place holder icon may have
+ // been set. Clear it.
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ return;
+ }
+
+ if (!mIconImage) {
+ // Set a placeholder icon, so that the menuitem reserves space for the icon during the load and
+ // there is no sudden shift once the icon finishes loading.
+ NSSize iconSize = NSMakeSize(kIconSize, kIconSize);
+ mIconImage = [[MOZIconHelper placeholderIconWithSize:iconSize] retain];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool nsMenuItemIconX::StartIconLoad(nsIContent* aContent) {
+ RefPtr<nsIURI> iconURI = GetIconURI(aContent);
+ if (!iconURI) {
+ return false;
+ }
+
+ if (!mIconLoader) {
+ mIconLoader = new IconLoader(this);
+ }
+
+ nsresult rv = mIconLoader->LoadIcon(iconURI, aContent);
+ return NS_SUCCEEDED(rv);
+}
+
+already_AddRefed<nsIURI> nsMenuItemIconX::GetIconURI(nsIContent* aContent) {
+ // First, look at the content node's "image" attribute.
+ nsAutoString imageURIString;
+ bool hasImageAttr =
+ aContent->IsElement() &&
+ aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::image, imageURIString);
+
+ if (hasImageAttr) {
+ // Use the URL from the image attribute.
+ // If this menu item shouldn't have an icon, the string will be empty,
+ // and NS_NewURI will fail.
+ RefPtr<nsIURI> iconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+ return iconURI.forget();
+ }
+
+ // If the content node has no "image" attribute, get the
+ // "list-style-image" property from CSS.
+ RefPtr<mozilla::dom::Document> document = aContent->GetComposedDoc();
+ if (!document || !aContent->IsElement()) {
+ return nullptr;
+ }
+
+ RefPtr<const ComputedStyle> sc = nsComputedDOMStyle::GetComputedStyle(aContent->AsElement());
+ if (!sc) {
+ return nullptr;
+ }
+
+ RefPtr<nsIURI> iconURI = sc->StyleList()->GetListStyleImageURI();
+ if (!iconURI) {
+ return nullptr;
+ }
+
+ mComputedStyle = std::move(sc);
+ mPresContext = document->GetPresContext();
+
+ return iconURI.forget();
+}
+
+//
+// mozilla::widget::IconLoader::Listener
+//
+
+nsresult nsMenuItemIconX::OnComplete(imgIContainer* aImage) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ RefPtr<nsPresContext> pc = mPresContext.get();
+ mIconImage = [[MOZIconHelper iconImageFromImageContainer:aImage
+ withSize:NSMakeSize(kIconSize, kIconSize)
+ presContext:pc
+ computedStyle:mComputedStyle
+ scaleFactor:0.0f] retain];
+ mComputedStyle = nullptr;
+ mPresContext = nullptr;
+
+ if (mListener) {
+ mListener->IconUpdated();
+ }
+
+ mIconLoader->Destroy();
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
diff --git a/widget/cocoa/nsMenuItemX.h b/widget/cocoa/nsMenuItemX.h
new file mode 100644
index 0000000000..980105e123
--- /dev/null
+++ b/widget/cocoa/nsMenuItemX.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuItemX_h_
+#define nsMenuItemX_h_
+
+#include "mozilla/RefPtr.h"
+#include "nsISupports.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemIconX.h"
+#include "nsChangeObserver.h"
+#include "nsStringFwd.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsMenuItemIconX;
+class nsMenuX;
+class nsMenuParentX;
+
+namespace mozilla {
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+enum {
+ knsMenuItemNoModifier = 0,
+ knsMenuItemShiftModifier = (1 << 0),
+ knsMenuItemAltModifier = (1 << 1),
+ knsMenuItemControlModifier = (1 << 2),
+ knsMenuItemCommandModifier = (1 << 3)
+};
+
+enum EMenuItemType {
+ eRegularMenuItemType = 0,
+ eCheckboxMenuItemType,
+ eRadioMenuItemType,
+ eSeparatorMenuItemType
+};
+
+// Once instantiated, this object lives until its DOM node or its parent window
+// is destroyed. Do not hold references to this, they can become invalid any
+// time the DOM node can be destroyed.
+class nsMenuItemX final : public nsChangeObserver,
+ public nsMenuItemIconX::Listener {
+ public:
+ nsMenuItemX(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType,
+ nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode);
+
+ bool IsVisible() const { return mIsVisible; }
+
+ // Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group
+ // owner pointer. This is needed because nsMenuX is reference-counted and can
+ // outlive its owner, and the menu group owner asserts that everything has
+ // been unregistered when it is destroyed.
+ void DetachFromGroupOwner();
+
+ // Nulls out our reference to the parent.
+ // This is needed because nsMenuX is reference-counted and can outlive its
+ // parent.
+ void DetachFromParent() { mMenuParent = nullptr; }
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuItemX)
+
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuItemX
+ nsresult SetChecked(bool aIsChecked);
+ EMenuItemType GetMenuItemType();
+ void DoCommand(NSEventModifierFlags aModifierFlags, int16_t aButton);
+ nsresult DispatchDOMEvent(const nsString& eventName,
+ bool* preventDefaultCalled);
+ void SetupIcon();
+ nsMenuX* ParentMenu() { return mMenuParent; }
+ nsIContent* Content() { return mContent; }
+ NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
+
+ void Dump(uint32_t aIndent) const;
+
+ protected:
+ virtual ~nsMenuItemX();
+
+ void UncheckRadioSiblings(nsIContent* aCheckedElement);
+ void SetKeyEquiv();
+
+ nsCOMPtr<nsIContent> mContent; // XUL <menuitem> or <menuseparator>
+
+ EMenuItemType mType;
+
+ // nsMenuItemX objects should always have a valid native menu item.
+ NSMenuItem* mNativeMenuItem = nil; // [strong]
+ nsMenuX* mMenuParent = nullptr; // [weak]
+ nsMenuGroupOwnerX* mMenuGroupOwner = nullptr; // [weak]
+ RefPtr<mozilla::dom::Element> mCommandElement;
+ mozilla::UniquePtr<nsMenuItemIconX> mIcon; // always non-null
+ bool mIsChecked = false;
+ bool mIsVisible = false;
+};
+
+#endif // nsMenuItemX_h_
diff --git a/widget/cocoa/nsMenuItemX.mm b/widget/cocoa/nsMenuItemX.mm
new file mode 100644
index 0000000000..175dad525c
--- /dev/null
+++ b/widget/cocoa/nsMenuItemX.mm
@@ -0,0 +1,401 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsMenuItemX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemIconX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+
+#include "nsObjCExceptions.h"
+
+#include "nsCOMPtr.h"
+#include "nsGkAtoms.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+
+using mozilla::dom::CallerType;
+using mozilla::dom::Event;
+
+nsMenuItemX::nsMenuItemX(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType,
+ nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
+ : mContent(aNode), mType(aItemType), mMenuParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsMenuItemX);
+
+ MOZ_RELEASE_ASSERT(mContent->IsElement(), "nsMenuItemX should only be created for elements");
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
+
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ dom::Document* doc = mContent->GetUncomposedDoc();
+
+ // if we have a command associated with this menu item, register for changes
+ // to the command DOM node
+ if (doc) {
+ nsAutoString ourCommand;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand);
+
+ if (!ourCommand.IsEmpty()) {
+ dom::Element* commandElement = doc->GetElementById(ourCommand);
+
+ if (commandElement) {
+ mCommandElement = commandElement;
+ // register to observe the command DOM element
+ mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this);
+ }
+ }
+ }
+
+ // decide enabled state based on command content if it exists, otherwise do it based
+ // on our own content
+ bool isEnabled;
+ if (mCommandElement) {
+ isEnabled = !mCommandElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters);
+ } else {
+ isEnabled = !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters);
+ }
+
+ // set up the native menu item
+ if (mType == eSeparatorMenuItemType) {
+ mNativeMenuItem = [[NSMenuItem separatorItem] retain];
+ } else {
+ NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
+ mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
+ action:nil
+ keyEquivalent:@""];
+
+ mIsChecked = mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
+ nsGkAtoms::_true, eCaseMatters);
+
+ mNativeMenuItem.enabled = isEnabled;
+ mNativeMenuItem.state = mIsChecked ? NSOnState : NSOffState;
+
+ SetKeyEquiv();
+ }
+
+ mIcon = MakeUnique<nsMenuItemIconX>(this);
+
+ mIsVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ // All menu items share the same target and action, and are differentiated
+ // be a unique (representedObject, tag) pair.
+ mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ mNativeMenuItem.action = @selector(menuItemHit:);
+ mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+ mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
+
+ if (mIsVisible) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuItemX::~nsMenuItemX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // autorelease the native menu item so that anything else happening to this
+ // object happens before the native menu item actually dies
+ [mNativeMenuItem autorelease];
+
+ DetachFromGroupOwner();
+
+ MOZ_COUNT_DTOR(nsMenuItemX);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::DetachFromGroupOwner() {
+ if (mMenuGroupOwner) {
+ mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag);
+
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+ if (mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ }
+ }
+
+ mMenuGroupOwner = nullptr;
+}
+
+nsresult nsMenuItemX::SetChecked(bool aIsChecked) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mIsChecked = aIsChecked;
+
+ // update the content model. This will also handle unchecking our siblings
+ // if we are a radiomenu
+ if (mIsChecked) {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true);
+ } else {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
+ }
+
+ // update native menu item
+ mNativeMenuItem.state = mIsChecked ? NSOnState : NSOffState;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; }
+
+// Executes the "cached" javaScript command.
+// Returns NS_OK if the command was executed properly, otherwise an error code.
+void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags, int16_t aButton) {
+ // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
+ if (mType == eCheckboxMenuItemType || (mType == eRadioMenuItemType && !mIsChecked)) {
+ if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
+ nsGkAtoms::_false, eCaseMatters)) {
+ SetChecked(!mIsChecked);
+ }
+ /* the AttributeChanged code will update all the internal state */
+ }
+
+ nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
+}
+
+nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName, bool* preventDefaultCalled) {
+ if (!mContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get owner document for content
+ nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
+
+ // create DOM event
+ ErrorResult rv;
+ RefPtr<Event> event = parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to create Event");
+ return rv.StealNSResult();
+ }
+ event->InitEvent(eventName, true, true);
+
+ // mark DOM event as trusted
+ event->SetTrusted(true);
+
+ // send DOM event
+ *preventDefaultCalled = mContent->DispatchEvent(*event, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to send DOM event via EventTarget");
+ return rv.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+// Walk the sibling list looking for nodes with the same name and
+// uncheck them all.
+void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) {
+ nsAutoString myGroupName;
+ aCheckedContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName);
+ if (!myGroupName.Length()) { // no groupname, nothing to do
+ return;
+ }
+
+ nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ // loop over siblings
+ for (nsIContent* sibling = parent->GetFirstChild(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (sibling != aCheckedContent && sibling->IsElement()) { // skip this node
+ // if the current sibling is in the same group, clear it
+ if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, myGroupName,
+ eCaseMatters)) {
+ sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"false"_ns, true);
+ }
+ }
+ }
+}
+
+void nsMenuItemX::SetKeyEquiv() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Set key shortcut and modifiers
+ nsAutoString keyValue;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue);
+
+ if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) {
+ dom::Element* keyContent = mContent->GetUncomposedDoc()->GetElementById(keyValue);
+ if (keyContent) {
+ nsAutoString keyChar;
+ bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
+
+ if (!hasKey || keyChar.IsEmpty()) {
+ nsAutoString keyCodeName;
+ keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName);
+ uint32_t charCode = nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
+ if (charCode) {
+ keyChar.Assign(charCode);
+ } else {
+ keyChar.AssignLiteral(u" ");
+ }
+ }
+
+ nsAutoString modifiersStr;
+ keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
+ uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+
+ unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
+ mNativeMenuItem.keyEquivalentModifierMask = macModifiers;
+
+ NSString* keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get()
+ length:keyChar.Length()] lowercaseString];
+ if ([keyEquivalent isEqualToString:@" "]) {
+ mNativeMenuItem.keyEquivalent = @"";
+ } else {
+ mNativeMenuItem.keyEquivalent = keyEquivalent;
+ }
+
+ return;
+ }
+ }
+
+ // if the key was removed, clear the key
+ mNativeMenuItem.keyEquivalent = @"";
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::Dump(uint32_t aIndent) const {
+ printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this,
+ mType == eSeparatorMenuItemType ? "----" : [mNativeMenuItem.title UTF8String],
+ NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
+}
+
+//
+// nsChangeObserver
+//
+
+void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent,
+ nsAtom* aAttribute) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (!aContent) {
+ return;
+ }
+
+ if (aContent == mContent) { // our own content node changed
+ if (aAttribute == nsGkAtoms::checked) {
+ // if we're a radio menu, uncheck our sibling radio items. No need to
+ // do any of this if we're just a normal check menu.
+ if (mType == eRadioMenuItemType &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
+ nsGkAtoms::_true, eCaseMatters)) {
+ UncheckRadioSiblings(mContent);
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) {
+ bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+ if (isVisible != mIsVisible) {
+ mIsVisible = isVisible;
+ RefPtr<nsMenuItemX> self = this;
+ mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self), isVisible);
+ if (mIsVisible) {
+ SetupIcon();
+ }
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::label) {
+ if (mType != eSeparatorMenuItemType) {
+ nsAutoString newLabel;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, newLabel);
+ mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel);
+ }
+ } else if (aAttribute == nsGkAtoms::key) {
+ SetKeyEquiv();
+ } else if (aAttribute == nsGkAtoms::image) {
+ SetupIcon();
+ } else if (aAttribute == nsGkAtoms::disabled) {
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ }
+ } else if (aContent == mCommandElement) {
+ // the only thing that really matters when the menu isn't showing is the
+ // enabled state since it enables/disables keyboard commands
+ if (aAttribute == nsGkAtoms::disabled) {
+ // first we sync our menu item DOM node with the command DOM node
+ nsAutoString commandDisabled;
+ nsAutoString menuDisabled;
+ aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled);
+ if (!commandDisabled.Equals(menuDisabled)) {
+ // The menu's disabled state needs to be updated to match the command.
+ if (commandDisabled.IsEmpty()) {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
+ } else {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled,
+ true);
+ }
+ }
+ // now we sync our native menu item with the command DOM node
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool IsMenuStructureElement(nsIContent* aContent) {
+ return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem,
+ nsGkAtoms::menuseparator);
+}
+
+void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild, nsIContent* aPreviousSibling) {
+ MOZ_RELEASE_ASSERT(mMenuGroupOwner);
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ if (aChild == mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ mCommandElement = nullptr;
+ }
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild) {
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ // The child node could come from the custom element that is for display, so
+ // only rebuild the menu if the child is related to the structure of the
+ // menu.
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::SetupIcon() {
+ if (mType != eRegularMenuItemType) {
+ // Don't support icons on checkbox and radio menuitems, for consistency with Windows & Linux.
+ return;
+ }
+
+ mIcon->SetupIcon(mContent);
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
+
+void nsMenuItemX::IconUpdated() { mNativeMenuItem.image = mIcon->GetIconImage(); }
diff --git a/widget/cocoa/nsMenuParentX.h b/widget/cocoa/nsMenuParentX.h
new file mode 100644
index 0000000000..a1f705631a
--- /dev/null
+++ b/widget/cocoa/nsMenuParentX.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuParentX_h_
+#define nsMenuParentX_h_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/Variant.h"
+
+class nsMenuX;
+class nsMenuBarX;
+class nsMenuItemX;
+
+// A base class for objects that can be the parent of an nsMenuX or nsMenuItemX.
+class nsMenuParentX {
+ public:
+ using MenuChild = mozilla::Variant<RefPtr<nsMenuX>, RefPtr<nsMenuItemX>>;
+
+ // XXXmstange double-check that this is still needed
+ virtual nsMenuBarX* AsMenuBar() { return nullptr; }
+
+ // Called when aChild becomes visible or hidden, so that the parent can insert
+ // or remove the child's native menu item from its NSMenu and update its state
+ // of visible items.
+ virtual void MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) = 0;
+};
+
+#endif // nsMenuParentX_h_
diff --git a/widget/cocoa/nsMenuUtilsX.h b/widget/cocoa/nsMenuUtilsX.h
new file mode 100644
index 0000000000..0ec8d8555d
--- /dev/null
+++ b/widget/cocoa/nsMenuUtilsX.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuUtilsX_h_
+#define nsMenuUtilsX_h_
+
+#include "nscore.h"
+#include "nsStringFwd.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsIContent;
+class nsMenuBarX;
+class nsMenuX;
+
+// Namespace containing utility functions used in our native menu implementation.
+namespace nsMenuUtilsX {
+void DispatchCommandTo(nsIContent* aTargetContent, NSEventModifierFlags aModifierFlags,
+ int16_t aButton);
+NSString* GetTruncatedCocoaLabel(const nsString& itemLabel);
+uint8_t GeckoModifiersForNodeAttribute(const nsString& modifiersAttribute);
+unsigned int MacModifiersForGeckoModifiers(uint8_t geckoModifiers);
+nsMenuBarX* GetHiddenWindowMenuBar(); // returned object is not retained
+NSMenuItem* GetStandardEditMenuItem(); // returned object is not retained
+bool NodeIsHiddenOrCollapsed(nsIContent* aContent);
+
+// Find the menu item by following the path aLocationString from aRootMenu.
+// aLocationString is a '|'-separated list of integers, where each integer is
+// the index of the menu item in the menu.
+// aIsMenuBar needs to be true if aRootMenu is the app's mainMenu, so that the
+// app menu can be skipped during the search.
+NSMenuItem* NativeMenuItemWithLocation(NSMenu* aRootMenu, NSString* aLocationString,
+ bool aIsMenuBar);
+
+// Traverse the menu tree and check that there are no cycles or NSMenu(Item) objects that are used
+// more than once. If inconsistencies are found, these functions crash the process.
+void CheckNativeMenuConsistency(NSMenu* aMenu);
+void CheckNativeMenuConsistency(NSMenuItem* aMenuItem);
+
+// Print out debugging information about the native menu tree structure.
+void DumpNativeMenu(NSMenu* aMenu);
+void DumpNativeMenuItem(NSMenuItem* aMenuItem);
+
+extern bool gIsSynchronouslyActivatingNativeMenuItemDuringTest;
+
+} // namespace nsMenuUtilsX
+
+#endif // nsMenuUtilsX_h_
diff --git a/widget/cocoa/nsMenuUtilsX.mm b/widget/cocoa/nsMenuUtilsX.mm
new file mode 100644
index 0000000000..aca279d64c
--- /dev/null
+++ b/widget/cocoa/nsMenuUtilsX.mm
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsMenuUtilsX.h"
+#include <unordered_set>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "NativeMenuMac.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaWindow.h"
+#include "nsGkAtoms.h"
+#include "nsGlobalWindowInner.h"
+#include "nsPIDOMWindow.h"
+#include "nsQueryObject.h"
+
+using namespace mozilla;
+
+bool nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = false;
+
+void nsMenuUtilsX::DispatchCommandTo(nsIContent* aTargetContent,
+ NSEventModifierFlags aModifierFlags, int16_t aButton) {
+ MOZ_ASSERT(aTargetContent, "null ptr");
+
+ dom::Document* doc = aTargetContent->OwnerDoc();
+ if (doc) {
+ RefPtr<dom::XULCommandEvent> event =
+ new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr);
+
+ bool ctrlKey = aModifierFlags & NSEventModifierFlagControl;
+ bool altKey = aModifierFlags & NSEventModifierFlagOption;
+ bool shiftKey = aModifierFlags & NSEventModifierFlagShift;
+ bool cmdKey = aModifierFlags & NSEventModifierFlagCommand;
+
+ IgnoredErrorResult rv;
+ event->InitCommandEvent(u"command"_ns, true, true,
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0, ctrlKey, altKey,
+ shiftKey, cmdKey, aButton, nullptr, 0, rv);
+ if (!rv.Failed()) {
+ event->SetTrusted(true);
+ aTargetContent->DispatchEvent(*event);
+ }
+ }
+}
+
+NSString* nsMenuUtilsX::GetTruncatedCocoaLabel(const nsString& itemLabel) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // We want to truncate long strings to some reasonable pixel length but there is no
+ // good API for doing that which works for all OS versions and architectures. For now
+ // we'll do nothing for consistency and depend on good user interface design to limit
+ // string lengths.
+ return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(itemLabel.get())
+ length:itemLabel.Length()];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+uint8_t nsMenuUtilsX::GeckoModifiersForNodeAttribute(const nsString& modifiersAttribute) {
+ uint8_t modifiers = knsMenuItemNoModifier;
+ char* str = ToNewCString(modifiersAttribute);
+ char* newStr;
+ char* token = strtok_r(str, ", \t", &newStr);
+ while (token != nullptr) {
+ if (strcmp(token, "shift") == 0) {
+ modifiers |= knsMenuItemShiftModifier;
+ } else if (strcmp(token, "alt") == 0) {
+ modifiers |= knsMenuItemAltModifier;
+ } else if (strcmp(token, "control") == 0) {
+ modifiers |= knsMenuItemControlModifier;
+ } else if ((strcmp(token, "accel") == 0) || (strcmp(token, "meta") == 0)) {
+ modifiers |= knsMenuItemCommandModifier;
+ }
+ token = strtok_r(newStr, ", \t", &newStr);
+ }
+ free(str);
+
+ return modifiers;
+}
+
+unsigned int nsMenuUtilsX::MacModifiersForGeckoModifiers(uint8_t geckoModifiers) {
+ unsigned int macModifiers = 0;
+
+ if (geckoModifiers & knsMenuItemShiftModifier) {
+ macModifiers |= NSEventModifierFlagShift;
+ }
+ if (geckoModifiers & knsMenuItemAltModifier) {
+ macModifiers |= NSEventModifierFlagOption;
+ }
+ if (geckoModifiers & knsMenuItemControlModifier) {
+ macModifiers |= NSEventModifierFlagControl;
+ }
+ if (geckoModifiers & knsMenuItemCommandModifier) {
+ macModifiers |= NSEventModifierFlagCommand;
+ }
+
+ return macModifiers;
+}
+
+nsMenuBarX* nsMenuUtilsX::GetHiddenWindowMenuBar() {
+ nsIWidget* hiddenWindowWidgetNoCOMPtr = nsCocoaUtils::GetHiddenWindowWidget();
+ if (hiddenWindowWidgetNoCOMPtr) {
+ return static_cast<nsCocoaWindow*>(hiddenWindowWidgetNoCOMPtr)->GetMenuBar();
+ }
+ return nullptr;
+}
+
+// It would be nice if we could localize these edit menu names.
+NSMenuItem* nsMenuUtilsX::GetStandardEditMenuItem() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // In principle we should be able to allocate this once and then always
+ // return the same object. But weird interactions happen between native
+ // app-modal dialogs and Gecko-modal dialogs that open above them. So what
+ // we return here isn't always released before it needs to be added to
+ // another menu. See bmo bug 468393.
+ NSMenuItem* standardEditMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Edit"
+ action:nil
+ keyEquivalent:@""] autorelease];
+ NSMenu* standardEditMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
+ standardEditMenuItem.submenu = standardEditMenu;
+ [standardEditMenu release];
+
+ // Add Undo
+ NSMenuItem* undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo"
+ action:@selector(undo:)
+ keyEquivalent:@"z"];
+ [standardEditMenu addItem:undoItem];
+ [undoItem release];
+
+ // Add Redo
+ NSMenuItem* redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo"
+ action:@selector(redo:)
+ keyEquivalent:@"Z"];
+ [standardEditMenu addItem:redoItem];
+ [redoItem release];
+
+ // Add separator
+ [standardEditMenu addItem:[NSMenuItem separatorItem]];
+
+ // Add Cut
+ NSMenuItem* cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
+ action:@selector(cut:)
+ keyEquivalent:@"x"];
+ [standardEditMenu addItem:cutItem];
+ [cutItem release];
+
+ // Add Copy
+ NSMenuItem* copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
+ action:@selector(copy:)
+ keyEquivalent:@"c"];
+ [standardEditMenu addItem:copyItem];
+ [copyItem release];
+
+ // Add Paste
+ NSMenuItem* pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
+ action:@selector(paste:)
+ keyEquivalent:@"v"];
+ [standardEditMenu addItem:pasteItem];
+ [pasteItem release];
+
+ // Add Delete
+ NSMenuItem* deleteItem = [[NSMenuItem alloc] initWithTitle:@"Delete"
+ action:@selector(delete:)
+ keyEquivalent:@""];
+ [standardEditMenu addItem:deleteItem];
+ [deleteItem release];
+
+ // Add Select All
+ NSMenuItem* selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All"
+ action:@selector(selectAll:)
+ keyEquivalent:@"a"];
+ [standardEditMenu addItem:selectAllItem];
+ [selectAllItem release];
+
+ return standardEditMenuItem;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool nsMenuUtilsX::NodeIsHiddenOrCollapsed(nsIContent* aContent) {
+ return aContent->IsElement() &&
+ (aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, nsGkAtoms::_true,
+ eCaseMatters) ||
+ aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed,
+ nsGkAtoms::_true, eCaseMatters));
+}
+
+NSMenuItem* nsMenuUtilsX::NativeMenuItemWithLocation(NSMenu* aRootMenu, NSString* aLocationString,
+ bool aIsMenuBar) {
+ NSArray<NSString*>* indexes = [aLocationString componentsSeparatedByString:@"|"];
+ unsigned int pathLength = indexes.count;
+ if (pathLength == 0) {
+ return nil;
+ }
+
+ NSMenu* currentSubmenu = aRootMenu;
+ for (unsigned int depth = 0; depth < pathLength; depth++) {
+ NSInteger targetIndex = [indexes objectAtIndex:depth].integerValue;
+ if (aIsMenuBar && depth == 0) {
+ // We remove the application menu from consideration for the top-level menu.
+ targetIndex++;
+ }
+ int itemCount = currentSubmenu.numberOfItems;
+ if (targetIndex >= itemCount) {
+ return nil;
+ }
+ NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex];
+ // if this is the last index just return the menu item
+ if (depth == pathLength - 1) {
+ return menuItem;
+ }
+ // if this is not the last index find the submenu and keep going
+ if (menuItem.hasSubmenu) {
+ currentSubmenu = menuItem.submenu;
+ } else {
+ return nil;
+ }
+ }
+
+ return nil;
+}
+
+static void CheckNativeMenuConsistencyImpl(NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects);
+
+static void CheckNativeMenuItemConsistencyImpl(NSMenuItem* aMenuItem,
+ std::unordered_set<void*>& aSeenObjects) {
+ bool inserted = aSeenObjects.insert(aMenuItem).second;
+ MOZ_RELEASE_ASSERT(inserted, "Duplicate NSMenuItem object in native menu structure");
+ if (aMenuItem.hasSubmenu) {
+ CheckNativeMenuConsistencyImpl(aMenuItem.submenu, aSeenObjects);
+ }
+}
+
+static void CheckNativeMenuConsistencyImpl(NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects) {
+ bool inserted = aSeenObjects.insert(aMenu).second;
+ MOZ_RELEASE_ASSERT(inserted, "Duplicate NSMenu object in native menu structure");
+ for (NSMenuItem* item in aMenu.itemArray) {
+ CheckNativeMenuItemConsistencyImpl(item, aSeenObjects);
+ }
+}
+
+void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenu* aMenu) {
+ std::unordered_set<void*> seenObjects;
+ CheckNativeMenuConsistencyImpl(aMenu, seenObjects);
+}
+
+void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenuItem* aMenuItem) {
+ std::unordered_set<void*> seenObjects;
+ CheckNativeMenuItemConsistencyImpl(aMenuItem, seenObjects);
+}
+
+static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
+ const Maybe<int>& aIndexInParentMenu);
+
+static void DumpNativeNSMenuImpl(NSMenu* aMenu, uint32_t aIndent) {
+ printf("%*sNSMenu [%p] %-16s\n", aIndent * 2, "", aMenu,
+ (aMenu.title.length == 0 ? "(no title)" : aMenu.title.UTF8String));
+ int index = 0;
+ for (NSMenuItem* item in aMenu.itemArray) {
+ DumpNativeNSMenuItemImpl(item, aIndent + 1, Some(index));
+ index++;
+ }
+}
+
+static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
+ const Maybe<int>& aIndexInParentMenu) {
+ printf("%*s", aIndent * 2, "");
+ if (aIndexInParentMenu) {
+ printf("[%d] ", *aIndexInParentMenu);
+ }
+ printf("NSMenuItem [%p] %-16s%s\n", aItem,
+ aItem.isSeparatorItem ? "----"
+ : (aItem.title.length == 0 ? "(no title)" : aItem.title.UTF8String),
+ aItem.hasSubmenu ? " [hasSubmenu]" : "");
+ if (aItem.hasSubmenu) {
+ DumpNativeNSMenuImpl(aItem.submenu, aIndent + 1);
+ }
+}
+
+void nsMenuUtilsX::DumpNativeMenu(NSMenu* aMenu) { DumpNativeNSMenuImpl(aMenu, 0); }
+
+void nsMenuUtilsX::DumpNativeMenuItem(NSMenuItem* aMenuItem) {
+ DumpNativeNSMenuItemImpl(aMenuItem, 0, Nothing());
+}
diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h
new file mode 100644
index 0000000000..ff78196ea7
--- /dev/null
+++ b/widget/cocoa/nsMenuX.h
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsMenuX_h_
+#define nsMenuX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Variant.h"
+#include "nsISupports.h"
+#include "nsMenuParentX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemIconX.h"
+#include "nsCOMPtr.h"
+#include "nsChangeObserver.h"
+#include "nsThreadUtils.h"
+
+class nsMenuX;
+class nsMenuItemX;
+class nsIWidget;
+
+// MenuDelegate is used to receive Cocoa notifications for setting
+// up carbon events. Protocol is defined as of 10.6 SDK.
+@interface MenuDelegate : NSObject <NSMenuDelegate> {
+ nsMenuX* mGeckoMenu; // weak ref
+ NSMutableArray* mBlocksToRunWhenOpen;
+}
+- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu;
+- (void)runBlockWhenOpen:(void (^)())block;
+- (void)menu:(NSMenu*)menu willActivateItem:(NSMenuItem*)item;
+@property BOOL menuIsInMenubar;
+@end
+
+class nsMenuXObserver {
+ public:
+ // Called when a menu in this menu subtree opens, before popupshowing.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) = 0;
+
+ // Called when a menu in this menu subtree opened, after popupshown.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) = 0;
+
+ // Called before a menu item is activated.
+ virtual void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
+ mozilla::dom::Element* aMenuItemElement) = 0;
+
+ // Called when a menu in this menu subtree closed, after popuphidden.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuClosed(mozilla::dom::Element* aPopupElement) = 0;
+};
+
+// Once instantiated, this object lives until its DOM node or its parent window is destroyed.
+// Do not hold references to this, they can become invalid any time the DOM node can be destroyed.
+class nsMenuX final : public nsMenuParentX,
+ public nsChangeObserver,
+ public nsMenuItemIconX::Listener,
+ public nsMenuXObserver {
+ public:
+ using Observer = nsMenuXObserver;
+
+ // aParent is optional.
+ nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent);
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuX)
+
+ // If > 0, the OS is indexing all the app's menus (triggered by opening
+ // Help menu on Leopard and higher). There are some things that are
+ // unsafe to do while this is happening.
+ static int32_t sIndexingMenuLevel;
+
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuXObserver, to forward notifications from our children to our observer.
+ void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
+ mozilla::dom::Element* aMenuItemElement) override;
+ void OnMenuClosed(mozilla::dom::Element* aPopupElement) override;
+
+ bool IsVisible() const { return mVisible; }
+
+ // Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group owner pointer, on this
+ // nsMenuX and also all nested nsMenuX and nsMenuItemX objects.
+ // This is needed because nsMenuX is reference-counted and can outlive its owner, and the menu
+ // group owner asserts that everything has been unregistered when it is destroyed.
+ void DetachFromGroupOwnerRecursive();
+
+ // Nulls out our reference to the parent.
+ // This is needed because nsMenuX is reference-counted and can outlive its parent.
+ void DetachFromParent() { mParent = nullptr; }
+
+ mozilla::Maybe<MenuChild> GetItemAt(uint32_t aPos);
+ uint32_t GetItemCount();
+
+ mozilla::Maybe<MenuChild> GetVisibleItemAt(uint32_t aPos);
+ nsresult GetVisibleItemCount(uint32_t& aCount);
+
+ mozilla::Maybe<MenuChild> GetItemForElement(mozilla::dom::Element* aMenuChildElement);
+
+ // Asynchronously runs the command event on aItem, after the root menu has closed.
+ void ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
+ int16_t aButton);
+
+ bool IsOpenForGecko() const { return mIsOpenForGecko; }
+
+ // Fires the popupshowing event and returns whether the handler allows the popup to open.
+ // When calling this method, the caller must hold a strong reference to this object, because other
+ // references to this object can be dropped during the handling of the DOM event.
+ MOZ_CAN_RUN_SCRIPT bool OnOpen();
+
+ void PopupShowingEventWasSentAndApprovedExternally() { DidFirePopupShowing(); }
+
+ // Called from the menu delegate during menuWillOpen, or to simulate opening.
+ // Ignored if the menu is already considered open.
+ // When calling this method, the caller must hold a strong reference to this object, because other
+ // references to this object can be dropped during the handling of the DOM event.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void MenuOpened();
+
+ // Called from the menu delegate during menuDidClose, or to simulate closing.
+ // Ignored if the menu is already considered closed.
+ // When calling this method, the caller must hold a strong reference to this object, because other
+ // references to this object can be dropped during the handling of the DOM event.
+ void MenuClosed();
+
+ // Close the menu if it's open, and flush any pending popuphiding / popuphidden events.
+ bool Close();
+
+ // Called from the menu delegate during menu:willHighlightItem:.
+ // If called with Nothing(), it means that no item is highlighted.
+ // The index only accounts for visible items, i.e. items for which there exists an NSMenuItem* in
+ // mNativeMenu.
+ void OnHighlightedItemChanged(const mozilla::Maybe<uint32_t>& aNewHighlightedIndex);
+
+ // Called from the menu delegate before an item anywhere in this menu is activated.
+ // Called after MenuClosed().
+ void OnWillActivateItem(NSMenuItem* aItem);
+
+ void SetRebuild(bool aMenuEvent);
+ void SetupIcon();
+ nsIContent* Content() { return mContent; }
+ NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
+ GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
+
+ void SetIconListener(nsMenuItemIconX::Listener* aListener) { mIconListener = aListener; }
+ void ClearIconListener() { mIconListener = nullptr; }
+
+ // nsMenuParentX
+ void MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) override;
+
+ void Dump(uint32_t aIndent) const;
+
+ static bool IsXULHelpMenu(nsIContent* aMenuContent);
+ static bool IsXULWindowMenu(nsIContent* aMenuContent);
+
+ // Set an observer that gets notified of menu opening and closing.
+ // The menu does not keep a strong reference the observer. The observer must
+ // remove itself before it is destroyed.
+ void SetObserver(Observer* aObserver) { mObserver = aObserver; }
+
+ // Stop observing.
+ void ClearObserver() { mObserver = nullptr; }
+
+ protected:
+ virtual ~nsMenuX();
+
+ void RebuildMenu();
+ nsresult RemoveAll();
+ nsresult SetEnabled(bool aIsEnabled);
+ nsresult GetEnabled(bool* aIsEnabled);
+ already_AddRefed<nsIContent> GetMenuPopupContent();
+ void WillInsertChild(const MenuChild& aChild);
+ void WillRemoveChild(const MenuChild& aChild);
+ void AddMenuChild(MenuChild&& aChild);
+ void InsertMenuChild(MenuChild&& aChild);
+ void RemoveMenuChild(const MenuChild& aChild);
+ mozilla::Maybe<MenuChild> CreateMenuChild(nsIContent* aContent);
+ RefPtr<nsMenuItemX> CreateMenuItem(nsIContent* aMenuItemContent);
+ GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle);
+ void DidFirePopupShowing();
+
+ // Find the index at which aChild needs to be inserted into mMenuChildren such that mMenuChildren
+ // remains in correct content order, i.e. the order in mMenuChildren is the same as the order of
+ // the DOM children of our <menupopup>.
+ size_t FindInsertionIndex(const MenuChild& aChild);
+
+ // Calculates the index at which aChild's NSMenuItem should be inserted into our NSMenu.
+ // The order of NSMenuItems in the NSMenu is the same as the order of menu children in
+ // mMenuChildren; the only difference is that mMenuChildren contains both visible and invisible
+ // children, and the NSMenu only contains visible items. So the insertion index is equal to the
+ // number of visible previous siblings of aChild in mMenuChildren.
+ NSInteger CalculateNativeInsertionPoint(const MenuChild& aChild);
+
+ // Fires the popupshown event.
+ MOZ_CAN_RUN_SCRIPT void MenuOpenedAsync();
+
+ // Called from mPendingAsyncMenuCloseRunnable asynchronously after MenuClosed(), so that it runs
+ // after any potential menuItemHit calls for clicked menu items.
+ // Fires popuphiding and popuphidden events.
+ // When calling this method, the caller must hold a strong reference to this object, because other
+ // references to this object can be dropped during the handling of the DOM event.
+ MOZ_CAN_RUN_SCRIPT void MenuClosedAsync();
+
+ // If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to send out the pending
+ // popupshown event.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuOpenedRunnable();
+
+ // If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to send out pending
+ // popuphiding/popuphidden events.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuClosedRunnable();
+
+ // Make sure the NSMenu contains at least one item, even if mVisibleItemsCount is zero.
+ // Otherwise it won't open.
+ void InsertPlaceholderIfNeeded();
+ // Remove the placeholder before adding an item to mNativeNSMenu.
+ void RemovePlaceholderIfPresent();
+
+ nsCOMPtr<nsIContent> mContent; // XUL <menu> or <menupopup>
+
+ // Contains nsMenuX and nsMenuItemX objects
+ nsTArray<MenuChild> mMenuChildren;
+
+ nsString mLabel;
+ uint32_t mVisibleItemsCount = 0; // cache
+ nsMenuParentX* mParent = nullptr; // [weak]
+ nsMenuGroupOwnerX* mMenuGroupOwner = nullptr; // [weak]
+ nsMenuItemIconX::Listener* mIconListener = nullptr; // [weak]
+ mozilla::UniquePtr<nsMenuItemIconX> mIcon;
+
+ Observer* mObserver = nullptr; // non-owning pointer to our observer
+
+ // Non-null between a call to MenuOpened() and MenuOpenedAsync().
+ RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuOpenRunnable;
+
+ // Non-null between a call to MenuClosed() and MenuClosedAsync().
+ // This is asynchronous so that, if a menu item is clicked, we can fire popuphiding *after* we
+ // execute the menu item command. The macOS menu system calls menuWillClose *before* it calls
+ // menuItemHit.
+ RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuCloseRunnable;
+
+ struct PendingCommandEvent {
+ RefPtr<nsMenuItemX> mMenuItem;
+ NSEventModifierFlags mModifiers;
+ int16_t mButton;
+ };
+
+ // Any pending command events.
+ // These are queued by ActivateItemAfterClosing and run by MenuClosedAsync.
+ nsTArray<PendingCommandEvent> mPendingCommandEvents;
+
+ GeckoNSMenu* mNativeMenu = nil; // [strong]
+ MenuDelegate* mMenuDelegate = nil; // [strong]
+ // nsMenuX objects should always have a valid native menu item.
+ NSMenuItem* mNativeMenuItem = nil; // [strong]
+
+ // Nothing() if no item is highlighted. The index only accounts for visible items.
+ mozilla::Maybe<uint32_t> mHighlightedItemIndex;
+
+ bool mIsEnabled = true;
+ bool mNeedsRebuild = true;
+
+ // Whether the native NSMenu is considered open.
+ // Also affected by MenuOpened() / MenuClosed() calls for simulated opening / closing.
+ bool mIsOpen = false;
+
+ // Whether the popup is open from Gecko's perspective, based on popupshowing / popuphiding events.
+ bool mIsOpenForGecko = false;
+
+ bool mVisible = true;
+
+ // true between an OnOpen() call that returned true, and the subsequent call
+ // to MenuOpened().
+ bool mDidFirePopupshowingAndIsApprovedToOpen = false;
+};
+
+#endif // nsMenuX_h_
diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm
new file mode 100644
index 0000000000..a0bd714249
--- /dev/null
+++ b/widget/cocoa/nsMenuX.mm
@@ -0,0 +1,1403 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsMenuX.h"
+
+#include <_types/_uint32_t.h>
+#include <dlfcn.h>
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MouseEvents.h"
+
+#include "MOZMenuOpeningCoordinator.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsMenuItemIconX.h"
+
+#include "nsObjCExceptions.h"
+
+#include "nsComputedDOMStyle.h"
+#include "nsThreadUtils.h"
+#include "nsToolkit.h"
+#include "nsCocoaUtils.h"
+#include "nsCOMPtr.h"
+#include "prinrval.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsGkAtoms.h"
+#include "nsCRT.h"
+#include "nsBaseWidget.h"
+
+#include "nsIContent.h"
+#include "nsIDocumentObserver.h"
+#include "nsIComponentManager.h"
+#include "nsIRollupListener.h"
+#include "nsIServiceManager.h"
+#include "nsXULPopupManager.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static bool gConstructingMenu = false;
+static bool gMenuMethodsSwizzled = false;
+
+int32_t nsMenuX::sIndexingMenuLevel = 0;
+
+// TODO: It is unclear whether this is still needed.
+static void SwizzleDynamicIndexingMethods() {
+ if (gMenuMethodsSwizzled) {
+ return;
+ }
+
+ nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:),
+ @selector(nsMenuX_NSMenu_addItem:toTable:), true);
+ nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:),
+ @selector(nsMenuX_NSMenu_removeItem:fromTable:), true);
+ // On SnowLeopard the Shortcut framework (which contains the
+ // SCTGRLIndex class) is loaded on demand, whenever the user first opens
+ // a menu (which normally hasn't happened yet). So we need to load it
+ // here explicitly.
+ dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut", RTLD_LAZY);
+ Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex");
+ nsToolkit::SwizzleMethods(SCTGRLIndexClass, @selector(indexMenuBarDynamically),
+ @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically));
+
+ Class NSServicesMenuUpdaterClass = ::NSClassFromString(@"_NSServicesMenuUpdater");
+ nsToolkit::SwizzleMethods(NSServicesMenuUpdaterClass,
+ @selector(populateMenu:withServiceEntries:forDisplay:),
+ @selector(nsMenuX_populateMenu:withServiceEntries:forDisplay:));
+
+ gMenuMethodsSwizzled = true;
+}
+
+//
+// nsMenuX
+//
+
+nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent)
+ : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsMenuX);
+
+ SwizzleDynamicIndexingMethods();
+
+ mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
+ mMenuDelegate.menuIsInMenubar = mMenuGroupOwner->GetMenuBar() != nullptr;
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
+ if (mContent->IsElement()) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel);
+ }
+ mNativeMenu = CreateMenuWithGeckoString(mLabel);
+
+ // register this menu to be notified when changes are made to our content object
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
+ mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
+ action:nil
+ keyEquivalent:@""];
+ mNativeMenuItem.submenu = mNativeMenu;
+
+ SetEnabled(!mContent->IsElement() ||
+ !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters));
+
+ // We call RebuildMenu here because keyboard commands are dependent upon
+ // native menu items being created. If we only call RebuildMenu when a menu
+ // is actually selected, then we can't access keyboard commands until the
+ // menu gets selected, which is bad.
+ RebuildMenu();
+
+ if (IsXULWindowMenu(mContent)) {
+ // Let the OS know that this is our Window menu.
+ NSApp.windowsMenu = mNativeMenu;
+ }
+
+ mIcon = MakeUnique<nsMenuItemIconX>(this);
+
+ if (mVisible) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuX::~nsMenuX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Make sure a pending popupshown event isn't dropped.
+ FlushMenuOpenedRunnable();
+
+ if (mIsOpen) {
+ [mNativeMenu cancelTracking];
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+ }
+
+ // Make sure pending popuphiding/popuphidden events aren't dropped.
+ FlushMenuClosedRunnable();
+
+ OnHighlightedItemChanged(Nothing());
+ RemoveAll();
+
+ mNativeMenu.delegate = nil;
+ [mNativeMenu release];
+ [mMenuDelegate release];
+ // autorelease the native menu item so that anything else happening to this
+ // object happens before the native menu item actually dies
+ [mNativeMenuItem autorelease];
+
+ DetachFromGroupOwnerRecursive();
+
+ MOZ_COUNT_DTOR(nsMenuX);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::DetachFromGroupOwnerRecursive() {
+ if (!mMenuGroupOwner) {
+ // Don't recurse if this subtree is already detached.
+ // This avoids repeated recursion during the destruction of nested nsMenuX structures.
+ // Our invariant is: If we are detached, all of our contents are also detached.
+ return;
+ }
+
+ if (mMenuGroupOwner && mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+ mMenuGroupOwner = nullptr;
+
+ // Also detach all our children.
+ for (auto& child : mMenuChildren) {
+ child.match([](const RefPtr<nsMenuX>& aMenu) { aMenu->DetachFromGroupOwnerRecursive(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->DetachFromGroupOwner(); });
+ }
+}
+
+void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuWillOpen(aPopupElement);
+ }
+}
+
+void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuDidOpen(aPopupElement);
+ }
+}
+
+void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement, dom::Element* aMenuItemElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
+ }
+}
+
+void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuClosed(aPopupElement);
+ }
+}
+
+void nsMenuX::AddMenuChild(MenuChild&& aChild) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ WillInsertChild(aChild);
+ mMenuChildren.AppendElement(aChild);
+
+ bool isVisible =
+ aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
+ NSMenuItem* nativeItem = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
+
+ if (isVisible) {
+ RemovePlaceholderIfPresent();
+ [mNativeMenu addItem:nativeItem];
+ ++mVisibleItemsCount;
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ WillInsertChild(aChild);
+ size_t insertionIndex = FindInsertionIndex(aChild);
+ mMenuChildren.InsertElementAt(insertionIndex, aChild);
+
+ bool isVisible =
+ aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
+ if (isVisible) {
+ MenuChildChangedVisibility(aChild, true);
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
+ bool isVisible =
+ aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
+ if (isVisible) {
+ MenuChildChangedVisibility(aChild, false);
+ }
+
+ WillRemoveChild(aChild);
+ mMenuChildren.RemoveElement(aChild);
+}
+
+size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
+ nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
+ MOZ_RELEASE_ASSERT(menuPopup);
+
+ RefPtr<nsIContent> insertedContent =
+ aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+
+ MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
+
+ // Iterate over menuPopup's children (insertedContent's siblings) until we encounter
+ // insertedContent. At the same time, keep track of the index in mMenuChildren.
+ size_t index = 0;
+ for (nsIContent* child = menuPopup->GetFirstChild(); child && index < mMenuChildren.Length();
+ child = child->GetNextSibling()) {
+ if (child == insertedContent) {
+ break;
+ }
+
+ RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (child == contentAtIndex) {
+ index++;
+ }
+ }
+
+ return index;
+}
+
+// Includes all items, including hidden/collapsed ones
+uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
+
+// Includes all items, including hidden/collapsed ones
+mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
+ if (aPos >= (uint32_t)mMenuChildren.Length()) {
+ return {};
+ }
+
+ return Some(mMenuChildren[aPos]);
+}
+
+// Only includes visible items
+nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
+ aCount = mVisibleItemsCount;
+ return NS_OK;
+}
+
+// Only includes visible items. Note that this is provides O(N) access
+// If you need to iterate or search, consider using GetItemAt and doing your own filtering
+Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
+ uint32_t count = mMenuChildren.Length();
+ if (aPos >= mVisibleItemsCount || aPos >= count) {
+ return {};
+ }
+
+ // If there are no invisible items, can provide direct access
+ if (mVisibleItemsCount == count) {
+ return GetItemAt(aPos);
+ }
+
+ // Otherwise, traverse the array until we find the the item we're looking for.
+ uint32_t visibleNodeIndex = 0;
+ for (uint32_t i = 0; i < count; i++) {
+ MenuChild item = *GetItemAt(i);
+ RefPtr<nsIContent> content =
+ item.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ if (aPos == visibleNodeIndex) {
+ // we found the visible node we're looking for, return it
+ return Some(item);
+ }
+ visibleNodeIndex++;
+ }
+ }
+
+ return {};
+}
+
+Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(Element* aMenuChildElement) {
+ for (auto& child : mMenuChildren) {
+ RefPtr<nsIContent> content =
+ child.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
+ if (content == aMenuChildElement) {
+ return Some(child);
+ }
+ }
+ return {};
+}
+
+nsresult nsMenuX::RemoveAll() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ [mNativeMenu removeAllItems];
+
+ for (auto& child : mMenuChildren) {
+ WillRemoveChild(child);
+ }
+
+ mMenuChildren.Clear();
+ mVisibleItemsCount = 0;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::WillInsertChild(const MenuChild& aChild) {
+ if (aChild.is<RefPtr<nsMenuX>>()) {
+ aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
+ }
+}
+
+void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
+ aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) {
+ aMenu->DetachFromGroupOwnerRecursive();
+ aMenu->DetachFromParent();
+ aMenu->SetObserver(nullptr);
+ },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ aMenuItem->DetachFromGroupOwner();
+ aMenuItem->DetachFromParent();
+ });
+}
+
+void nsMenuX::MenuOpened() {
+ if (mIsOpen) {
+ return;
+ }
+
+ // Make sure we fire any pending popupshown / popuphiding / popuphidden events first.
+ FlushMenuOpenedRunnable();
+ FlushMenuClosedRunnable();
+
+ if (!mDidFirePopupshowingAndIsApprovedToOpen) {
+ // Fire popupshowing now.
+ bool approvedToOpen = OnOpen();
+ if (!approvedToOpen) {
+ // We can only stop menus from opening which we open ourselves. We cannot stop menubar root
+ // menus or menu submenus from opening.
+ // For context menus, we can call OnOpen() before we ask the system to open the menu.
+ NS_WARNING("The popupshowing event had preventDefault() called on it, but in MenuOpened() it "
+ "is too late to stop the menu from opening.");
+ }
+ }
+
+ mIsOpen = true;
+
+ // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
+ mDidFirePopupshowingAndIsApprovedToOpen = false;
+
+ if (mNeedsRebuild) {
+ OnHighlightedItemChanged(Nothing());
+ RemoveAll();
+ RebuildMenu();
+ }
+
+ // Fire the popupshown event in MenuOpenedAsync.
+ // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose
+ // will not be called.
+ // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
+ // reference cycle.
+ class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
+ public:
+ explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
+ : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
+ if (RefPtr<nsMenuX> menu = mMenu) {
+ menu->MenuOpenedAsync();
+ mMenu = nullptr;
+ }
+ return NS_OK;
+ }
+ nsresult Cancel() override {
+ mMenu = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsMenuX* mMenu; // weak, cleared by Cancel() and Run()
+ };
+ mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
+ NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
+}
+
+void nsMenuX::FlushMenuOpenedRunnable() {
+ if (mPendingAsyncMenuOpenRunnable) {
+ MenuOpenedAsync();
+ }
+}
+
+void nsMenuX::MenuOpenedAsync() {
+ if (mPendingAsyncMenuOpenRunnable) {
+ mPendingAsyncMenuOpenRunnable->Cancel();
+ mPendingAsyncMenuOpenRunnable = nullptr;
+ }
+
+ mIsOpenForGecko = true;
+
+ // Open the node.
+ if (mContent->IsElement()) {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
+ }
+
+ RefPtr<nsIContent> popupContent = GetMenuPopupContent();
+
+ // Notify our observer.
+ if (mObserver && popupContent) {
+ mObserver->OnMenuDidOpen(popupContent->AsElement());
+ }
+
+ // Fire popupshown.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal);
+ RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
+}
+
+void nsMenuX::MenuClosed() {
+ if (!mIsOpen) {
+ return;
+ }
+
+ // Make sure we fire any pending popupshown events first.
+ FlushMenuOpenedRunnable();
+
+ // If any of our submenus were opened programmatically, make sure they get closed first.
+ for (auto& child : mMenuChildren) {
+ if (child.is<RefPtr<nsMenuX>>()) {
+ child.as<RefPtr<nsMenuX>>()->MenuClosed();
+ }
+ }
+
+ mIsOpen = false;
+
+ // Do the rest of the MenuClosed work in MenuClosedAsync.
+ // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem was clicked,
+ // menuDidClose is called *before* menuItemHit for the clicked menu item is called.
+ // This runnable will be canceled if ~nsMenuX runs before the runnable.
+ // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
+ // reference cycle.
+ class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
+ public:
+ explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
+ : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
+ if (RefPtr<nsMenuX> menu = mMenu) {
+ menu->MenuClosedAsync();
+ mMenu = nullptr;
+ }
+ return NS_OK;
+ }
+ nsresult Cancel() override {
+ mMenu = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsMenuX* mMenu; // weak, cleared by Cancel() and Run()
+ };
+
+ mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
+
+ NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
+}
+
+void nsMenuX::FlushMenuClosedRunnable() {
+ // If any of our submenus have a pending menu closed runnable, make sure those run first.
+ for (auto& child : mMenuChildren) {
+ if (child.is<RefPtr<nsMenuX>>()) {
+ child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
+ }
+ }
+
+ if (mPendingAsyncMenuCloseRunnable) {
+ MenuClosedAsync();
+ }
+}
+
+void nsMenuX::MenuClosedAsync() {
+ if (mPendingAsyncMenuCloseRunnable) {
+ mPendingAsyncMenuCloseRunnable->Cancel();
+ mPendingAsyncMenuCloseRunnable = nullptr;
+ }
+
+ // If we have pending command events, run those first.
+ nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
+ for (auto& event : events) {
+ event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
+ }
+
+ // Make sure no item is highlighted.
+ OnHighlightedItemChanged(Nothing());
+
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr, WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr, &status);
+
+ mIsOpenForGecko = false;
+
+ if (mContent->IsElement()) {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
+ }
+
+ WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr, WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr, &status);
+
+ // Notify our observer.
+ if (mObserver && popupContent) {
+ mObserver->OnMenuClosed(popupContent->AsElement());
+ }
+}
+
+void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
+ int16_t aButton) {
+ if (mIsOpenForGecko) {
+ // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand in
+ // MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will run soon.
+ mPendingCommandEvents.AppendElement(PendingCommandEvent{std::move(aItem), aModifiers, aButton});
+ } else {
+ // The menu item was activated outside of a regular open / activate / close sequence.
+ // This happens in multiple cases:
+ // - When a menu item is activated by a keyboard shortcut while all windows are closed
+ // (otherwise those shortcuts go through Gecko's manual keyboard handling)
+ // - When a menu item in the Dock menu is clicked
+ // - During native menu tests
+ //
+ // Run the command synchronously.
+ aItem->DoCommand(aModifiers, aButton);
+ }
+}
+
+bool nsMenuX::Close() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
+ // Close is being called right after this menu was opened, but before MenuOpened() had a chance
+ // to run. Call it here so that we can go through the entire popupshown -> popuphiding ->
+ // popuphidden sequence. Some callers expect to get a popuphidden event even if they close the
+ // popup before it was fully open.
+ MenuOpened();
+ }
+
+ FlushMenuOpenedRunnable();
+
+ bool wasOpen = mIsOpenForGecko;
+
+ if (mIsOpen) {
+ // Close the menu.
+ // We usually don't get here during normal Firefox usage: If the user closes the menu by
+ // clicking an item, or by clicking outside the menu, or by pressing escape, then the menu gets
+ // closed by macOS, and not by a call to nsMenuX::Close().
+ // If we do get here, it's usually because we're running an automated test. Close the menu
+ // without the fade-out animation so that we don't unnecessarily slow down the automated tests.
+ [mNativeMenu cancelTrackingWithoutAnimation];
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+
+ // Handle closing synchronously.
+ MenuClosed();
+ }
+
+ FlushMenuClosedRunnable();
+
+ return wasOpen;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::OnHighlightedItemChanged(const Maybe<uint32_t>& aNewHighlightedIndex) {
+ if (mHighlightedItemIndex == aNewHighlightedIndex) {
+ return;
+ }
+
+ if (mHighlightedItemIndex) {
+ Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
+ if (target && target->is<RefPtr<nsMenuItemX>>()) {
+ bool handlerCalledPreventDefault; // but we don't actually care
+ target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemInactive"_ns,
+ &handlerCalledPreventDefault);
+ }
+ }
+ if (aNewHighlightedIndex) {
+ Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
+ if (target && target->is<RefPtr<nsMenuItemX>>()) {
+ bool handlerCalledPreventDefault; // but we don't actually care
+ target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemActive"_ns,
+ &handlerCalledPreventDefault);
+ }
+ }
+ mHighlightedItemIndex = aNewHighlightedIndex;
+}
+
+void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
+ if (!mIsOpenForGecko) {
+ return;
+ }
+
+ if (mMenuGroupOwner && mObserver) {
+ nsMenuItemX* item = mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
+ if (item && item->Content()->IsElement()) {
+ RefPtr<dom::Element> itemElement = item->Content()->AsElement();
+ if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
+ mObserver->OnMenuWillActivateItem(popupContent->AsElement(), itemElement);
+ }
+ }
+ }
+}
+
+// Flushes style.
+static NSUserInterfaceLayoutDirection DirectionForElement(dom::Element* aElement) {
+ // Get the direction from the computed style so that inheritance into submenus is respected.
+ // aElement may not have a frame.
+ RefPtr<const ComputedStyle> sc = nsComputedDOMStyle::GetComputedStyle(aElement);
+ if (!sc) {
+ return NSApp.userInterfaceLayoutDirection;
+ }
+
+ switch (sc->StyleVisibility()->mDirection) {
+ case StyleDirection::Ltr:
+ return NSUserInterfaceLayoutDirectionLeftToRight;
+ case StyleDirection::Rtl:
+ return NSUserInterfaceLayoutDirectionRightToLeft;
+ }
+}
+
+void nsMenuX::RebuildMenu() {
+ MOZ_RELEASE_ASSERT(mNeedsRebuild);
+ gConstructingMenu = true;
+
+ // Retrieve our menupopup.
+ nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
+ if (!menuPopup) {
+ gConstructingMenu = false;
+ return;
+ }
+
+ if (menuPopup->IsElement()) {
+ mNativeMenu.userInterfaceLayoutDirection = DirectionForElement(menuPopup->AsElement());
+ }
+
+ // Iterate over the kids
+ for (nsIContent* child = menuPopup->GetFirstChild(); child; child = child->GetNextSibling()) {
+ if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
+ AddMenuChild(std::move(*menuChild));
+ }
+ } // for each menu item
+
+ InsertPlaceholderIfNeeded();
+
+ gConstructingMenu = false;
+ mNeedsRebuild = false;
+}
+
+void nsMenuX::InsertPlaceholderIfNeeded() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if ([mNativeMenu numberOfItems] == 0) {
+ MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
+ NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
+ item.enabled = NO;
+ item.view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
+ [mNativeMenu addItem:item];
+ [item release];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::RemovePlaceholderIfPresent() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
+ // Remove the placeholder.
+ [mNativeMenu removeItemAtIndex:0];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::SetRebuild(bool aNeedsRebuild) {
+ if (!gConstructingMenu) {
+ mNeedsRebuild = aNeedsRebuild;
+ if (mParent && mParent->AsMenuBar()) {
+ mParent->AsMenuBar()->SetNeedsRebuild();
+ }
+ }
+}
+
+nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
+ if (aIsEnabled != mIsEnabled) {
+ // we always want to rebuild when this changes
+ mIsEnabled = aIsEnabled;
+ mNativeMenuItem.enabled = mIsEnabled;
+ }
+ return NS_OK;
+}
+
+nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ *aIsEnabled = mIsEnabled;
+ return NS_OK;
+}
+
+GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
+ length:aMenuTitle.Length()];
+ GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
+ myMenu.delegate = mMenuDelegate;
+
+ // We don't want this menu to auto-enable menu items because then Cocoa
+ // overrides our decisions and things get incorrectly enabled/disabled.
+ myMenu.autoenablesItems = NO;
+
+ // we used to install Carbon event handlers here, but since NSMenu* doesn't
+ // create its underlying MenuRef until just before display, we delay until
+ // that happens. Now we install the event handlers when Cocoa notifies
+ // us that a menu is about to display - see the Cocoa MenuDelegate class.
+
+ return myMenu;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
+ if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem, nsGkAtoms::menuseparator)) {
+ return Some(MenuChild(CreateMenuItem(aContent)));
+ }
+ if (aContent->IsXULElement(nsGkAtoms::menu)) {
+ return Some(MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
+ }
+ return {};
+}
+
+RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
+ MOZ_RELEASE_ASSERT(aMenuItemContent);
+
+ nsAutoString menuitemName;
+ if (aMenuItemContent->IsElement()) {
+ aMenuItemContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, menuitemName);
+ }
+
+ EMenuItemType itemType = eRegularMenuItemType;
+ if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
+ itemType = eSeparatorMenuItemType;
+ } else if (aMenuItemContent->IsElement()) {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr};
+ switch (aMenuItemContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
+ strings, eCaseMatters)) {
+ case 0:
+ itemType = eCheckboxMenuItemType;
+ break;
+ case 1:
+ itemType = eRadioMenuItemType;
+ break;
+ }
+ }
+
+ return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner, aMenuItemContent);
+}
+
+// This menu is about to open. Returns false if the handler wants to stop the opening of the menu.
+bool nsMenuX::OnOpen() {
+ if (mDidFirePopupshowingAndIsApprovedToOpen) {
+ return true;
+ }
+
+ if (mIsOpen) {
+ NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered to be open. This "
+ "seems odd.");
+ }
+
+ RefPtr<nsIContent> popupContent = GetMenuPopupContent();
+
+ if (mObserver && popupContent) {
+ mObserver->OnMenuWillOpen(popupContent->AsElement());
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShowing, nullptr, WidgetMouseEvent::eReal);
+
+ nsresult rv = NS_OK;
+ RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+ rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
+ if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
+ return false;
+ }
+
+ DidFirePopupShowing();
+
+ return true;
+}
+
+void nsMenuX::DidFirePopupShowing() {
+ mDidFirePopupshowingAndIsApprovedToOpen = true;
+
+ // If the open is going to succeed we need to walk our menu items, checking to
+ // see if any of them have a command attribute. If so, several attributes
+ // must potentially be updated.
+
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (!popupContent) {
+ return;
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ pm->UpdateMenuItems(popupContent->AsElement());
+ }
+}
+
+// Find the |menupopup| child in the |popup| representing this menu. It should be one
+// of a very few children so we won't be iterating over a bazillion menu items to find
+// it (so the strcmp won't kill us).
+already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
+ // Check to see if we are a "menupopup" node (if we are a native menu).
+ if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
+ return do_AddRef(mContent);
+ }
+
+ // Otherwise check our child nodes.
+
+ for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsXULElement(nsGkAtoms::menupopup)) {
+ return child.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
+ bool retval = false;
+ if (aMenuContent && aMenuContent->IsElement()) {
+ nsAutoString id;
+ aMenuContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
+ if (id.Equals(u"helpMenu"_ns)) {
+ retval = true;
+ }
+ }
+ return retval;
+}
+
+bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
+ bool retval = false;
+ if (aMenuContent && aMenuContent->IsElement()) {
+ nsAutoString id;
+ aMenuContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
+ if (id.Equals(u"windowMenu"_ns)) {
+ retval = true;
+ }
+ }
+ return retval;
+}
+
+//
+// nsChangeObserver
+//
+
+void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent,
+ nsAtom* aAttribute) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // ignore the |open| attribute, which is by far the most common
+ if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::disabled) {
+ SetEnabled(!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters));
+ } else if (aAttribute == nsGkAtoms::label) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel);
+ NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
+ mNativeMenu.title = newCocoaLabelString;
+ mNativeMenuItem.title = newCocoaLabelString;
+ } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) {
+ SetRebuild(true);
+
+ bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ // don't do anything if the state is correct already
+ if (newVisible == mVisible) {
+ return;
+ }
+
+ mVisible = newVisible;
+ if (mParent) {
+ RefPtr<nsMenuX> self = this;
+ mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
+ }
+ if (mVisible) {
+ SetupIcon();
+ }
+ } else if (aAttribute == nsGkAtoms::image) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild, nsIContent* aPreviousSibling) {
+ if (gConstructingMenu) {
+ return;
+ }
+
+ SetRebuild(true);
+ mMenuGroupOwner->UnregisterForContentChanges(aChild);
+
+ if (!mIsOpen) {
+ // We will update the menu contents the next time the menu is opened.
+ return;
+ }
+
+ // The menu is currently open. Remove the child from mMenuChildren and from our NSMenu.
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (popupContent && aContainer == popupContent && aChild->IsElement()) {
+ if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
+ RemoveMenuChild(*child);
+ }
+ }
+}
+
+void nsMenuX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer,
+ nsIContent* aChild) {
+ if (gConstructingMenu) {
+ return;
+ }
+
+ SetRebuild(true);
+
+ if (!mIsOpen) {
+ // We will update the menu contents the next time the menu is opened.
+ return;
+ }
+
+ // The menu is currently open. Insert the child into mMenuChildren and into our NSMenu.
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (popupContent && aContainer == popupContent) {
+ if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
+ InsertMenuChild(std::move(*child));
+ }
+ }
+}
+
+void nsMenuX::SetupIcon() {
+ mIcon->SetupIcon(mContent);
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
+
+void nsMenuX::IconUpdated() {
+ mNativeMenuItem.image = mIcon->GetIconImage();
+ if (mIconListener) {
+ mIconListener->IconUpdated();
+ }
+}
+
+void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSMenuItem* nativeItem = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
+ if (aIsVisible) {
+ MOZ_RELEASE_ASSERT(!nativeItem.menu,
+ "The native item should not be in a menu while it is hidden");
+ RemovePlaceholderIfPresent();
+ NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
+ [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
+ mVisibleItemsCount++;
+ } else {
+ MOZ_RELEASE_ASSERT([mNativeMenu indexOfItem:nativeItem] != -1,
+ "The native item should be in this menu while it is visible");
+ [mNativeMenu removeItem:nativeItem];
+ mVisibleItemsCount--;
+ InsertPlaceholderIfNeeded();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
+ NSInteger insertionPoint = 0;
+ for (auto& currItem : mMenuChildren) {
+ // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
+ if (currItem == aChild) {
+ return insertionPoint;
+ }
+ NSMenuItem* nativeItem = currItem.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
+ // Only count visible items.
+ if (nativeItem.menu) {
+ insertionPoint++;
+ }
+ }
+ return insertionPoint;
+}
+
+void nsMenuX::Dump(uint32_t aIndent) const {
+ printf("%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
+ mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
+ NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
+ if (mNeedsRebuild) {
+ printf(" [NeedsRebuild]");
+ }
+ if (mIsOpen) {
+ printf(" [Open]");
+ }
+ if (mVisible) {
+ printf(" [Visible]");
+ }
+ if (mIsEnabled) {
+ printf(" [IsEnabled]");
+ }
+ printf(" (%d visible items)", int(mVisibleItemsCount));
+ printf("\n");
+ for (const auto& subitem : mMenuChildren) {
+ subitem.match([=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
+ [=](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->Dump(aIndent + 1); });
+ }
+}
+
+//
+// MenuDelegate Objective-C class, used to set up Carbon events
+//
+
+@implementation MenuDelegate
+
+- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
+ if ((self = [super init])) {
+ NS_ASSERTION(geckoMenu,
+ "Cannot initialize native menu delegate with NULL gecko menu! Will crash!");
+ mGeckoMenu = geckoMenu;
+ mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [mBlocksToRunWhenOpen release];
+ [super dealloc];
+}
+
+- (void)runBlockWhenOpen:(void (^)())block {
+ [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
+}
+
+- (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
+ if (!aMenu || !mGeckoMenu) {
+ return;
+ }
+
+ Maybe<uint32_t> index =
+ aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem])) : Nothing();
+ mGeckoMenu->OnHighlightedItemChanged(index);
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ for (void (^block)() in mBlocksToRunWhenOpen) {
+ block();
+ }
+ [mBlocksToRunWhenOpen removeAllObjects];
+
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Don't do anything while the OS is (re)indexing our menus (on Leopard and
+ // higher). This stops the Help menu from being able to search in our
+ // menus, but it also resolves many other problems.
+ if (nsMenuX::sIndexingMenuLevel > 0) {
+ return;
+ }
+
+ if (self.menuIsInMenubar) {
+ // If a menu in the menubar is trying open while a non-native menu is open, roll up the
+ // non-native menu and reject the menubar opening attempt, effectively consuming the event.
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ if (rollupListener) {
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (rollupWidget) {
+ rollupListener->Rollup({0, nsIRollupListener::FlushViews::Yes});
+ [menu cancelTracking];
+ return;
+ }
+ }
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->MenuOpened();
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Don't do anything while the OS is (re)indexing our menus (on Leopard and
+ // higher). This stops the Help menu from being able to search in our
+ // menus, but it also resolves many other problems.
+ if (nsMenuX::sIndexingMenuLevel > 0) {
+ return;
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->MenuClosed();
+}
+
+// This is called after menuDidClose:.
+- (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->OnWillActivateItem(aItem);
+}
+
+@end
+
+// OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
+// behavior that's present in Mozilla.org browsers but not (as best I can
+// tell) in Apple products like Safari. (It's not yet clear exactly what this
+// behavior is.)
+//
+// The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
+// call to [NSMenu removeItemAtIndex:]. The crash is caused by trying to
+// access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
+// to send it a _setChangedFlags: message). Though this object was deleted
+// some time ago, it remains registered as a potential target for a particular
+// key equivalent. So when [NSMenu removeItemAtIndex:] removes the current
+// target for that same key equivalent, the OS tries to "activate" the
+// previous target.
+//
+// The underlying reason appears to be that NSMenu's _addItem:toTable: and
+// _removeItem:fromTable: methods (which are used to keep a hashtable of
+// registered key equivalents) don't properly "retain" and "release"
+// NSMenuItem objects as they are added to and removed from the hashtable.
+//
+// Our (hackish) workaround is to shadow the OS's hashtable with another
+// hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
+// "release" NSMenuItem objects as needed. This resolves bmo bugs 422287 and
+// 423669. When (if) Apple fixes this bug, we can remove this workaround.
+
+static NSMutableDictionary* gShadowKeyEquivDB = nil;
+
+// Class for values in gShadowKeyEquivDB.
+
+@interface KeyEquivDBItem : NSObject {
+ NSMenuItem* mItem;
+ NSMutableSet* mTables;
+}
+
+- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
+- (BOOL)hasTable:(NSMapTable*)aTable;
+- (int)addTable:(NSMapTable*)aTable;
+- (int)removeTable:(NSMapTable*)aTable;
+
+@end
+
+@implementation KeyEquivDBItem
+
+- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
+ if (!gShadowKeyEquivDB) {
+ gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
+ }
+ self = [super init];
+ if (aItem && aTable) {
+ mTables = [[NSMutableSet alloc] init];
+ mItem = [aItem retain];
+ [mTables addObject:[NSValue valueWithPointer:aTable]];
+ } else {
+ mTables = nil;
+ mItem = nil;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (mTables) {
+ [mTables release];
+ }
+ if (mItem) {
+ [mItem release];
+ }
+ [super dealloc];
+}
+
+- (BOOL)hasTable:(NSMapTable*)aTable {
+ return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
+}
+
+// Does nothing if aTable (its index value) is already present in mTables.
+- (int)addTable:(NSMapTable*)aTable {
+ if (aTable) {
+ [mTables addObject:[NSValue valueWithPointer:aTable]];
+ }
+ return [mTables count];
+}
+
+- (int)removeTable:(NSMapTable*)aTable {
+ if (aTable) {
+ NSValue* objectToRemove = [mTables member:[NSValue valueWithPointer:aTable]];
+ if (objectToRemove) {
+ [mTables removeObject:objectToRemove];
+ }
+ }
+ return [mTables count];
+}
+
+@end
+
+@interface NSMenu (MethodSwizzling)
++ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
++ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable;
+@end
+
+@implementation NSMenu (MethodSwizzling)
+
++ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
+ if (aItem && aTable) {
+ NSValue* key = [NSValue valueWithPointer:aItem];
+ KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
+ if (shadowItem) {
+ [shadowItem addTable:aTable];
+ } else {
+ shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
+ [gShadowKeyEquivDB setObject:shadowItem forKey:key];
+ // Release after [NSMutableDictionary setObject:forKey:] retains it (so
+ // that it will get dealloced when removeObjectForKey: is called).
+ [shadowItem release];
+ }
+ }
+
+ [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
+}
+
++ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable {
+ [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
+
+ if (aItem && aTable) {
+ NSValue* key = [NSValue valueWithPointer:aItem];
+ KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
+ if (shadowItem && [shadowItem hasTable:aTable]) {
+ if (![shadowItem removeTable:aTable]) {
+ [gShadowKeyEquivDB removeObjectForKey:key];
+ }
+ }
+ }
+}
+
+@end
+
+// This class is needed to keep track of when the OS is (re)indexing all of
+// our menus. This appears to only happen on Leopard and higher, and can
+// be triggered by opening the Help menu. Some operations are unsafe while
+// this is happening -- notably the calls to [[NSImage alloc]
+// initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
+// OnStopFrame(). But we don't yet have a complete list, and Apple doesn't
+// yet have any documentation on this subject. (Apple also doesn't yet have
+// any documented way to find the information we seek here.) The "original"
+// of this class (the one whose indexMenuBarDynamically method we hook) is
+// defined in the Shortcut framework in /System/Library/PrivateFrameworks.
+@interface NSObject (SCTGRLIndexMethodSwizzling)
+- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
+@end
+
+@implementation NSObject (SCTGRLIndexMethodSwizzling)
+
+- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
+ // This method appears to be called (once) whenever the OS (re)indexes our
+ // menus. sIndexingMenuLevel is a int32_t just in case it might be
+ // reentered. As it's running, it spawns calls to two undocumented
+ // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
+ // which "simulate" the opening and closing of our menus without actually
+ // displaying them.
+ ++nsMenuX::sIndexingMenuLevel;
+ [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
+ --nsMenuX::sIndexingMenuLevel;
+}
+
+@end
+
+@interface NSObject (NSServicesMenuUpdaterSwizzling)
+- (void)nsMenuX_populateMenu:(NSMenu*)aMenu
+ withServiceEntries:(NSArray*)aServices
+ forDisplay:(BOOL)aForDisplay;
+@end
+
+@interface _NSServiceEntry : NSObject
+- (NSString*)bundleIdentifier;
+@end
+
+@implementation NSObject (NSServicesMenuUpdaterSwizzling)
+
+- (void)nsMenuX_populateMenu:(NSMenu*)aMenu
+ withServiceEntries:(NSArray*)aServices
+ forDisplay:(BOOL)aForDisplay {
+ NSMutableArray* filteredServices = [NSMutableArray array];
+
+ // We need to filter some services, such as "Search with Google", since this
+ // service is duplicating functionality already exposed by our "Search Google
+ // for..." context menu entry and because it opens in Safari, which can cause
+ // confusion for users.
+ for (_NSServiceEntry* service in aServices) {
+ NSString* bundleId = [service bundleIdentifier];
+ NSString* msg = [service valueForKey:@"message"];
+ bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
+ ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
+ [msg isEqualToString:@"openURL"]);
+ if (!shouldSkip) {
+ [filteredServices addObject:service];
+ }
+ }
+
+ [self nsMenuX_populateMenu:aMenu withServiceEntries:filteredServices forDisplay:aForDisplay];
+}
+
+@end
diff --git a/widget/cocoa/nsNativeThemeCocoa.h b/widget/cocoa/nsNativeThemeCocoa.h
new file mode 100644
index 0000000000..a6c9812238
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeCocoa.h
@@ -0,0 +1,419 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsNativeThemeCocoa_h_
+#define nsNativeThemeCocoa_h_
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/Variant.h"
+
+#include "nsITheme.h"
+#include "ThemeCocoa.h"
+#include "mozilla/dom/RustTypes.h"
+
+@class MOZCellDrawWindow;
+@class MOZCellDrawView;
+@class MOZSearchFieldCell;
+@class NSProgressBarCell;
+class nsDeviceContext;
+struct SegmentedControlRenderSettings;
+
+namespace mozilla {
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+} // namespace mozilla
+
+class nsNativeThemeCocoa : public mozilla::widget::ThemeCocoa {
+ using ThemeCocoa = mozilla::widget::ThemeCocoa;
+
+ public:
+ enum class MenuIcon : uint8_t {
+ eCheckmark,
+ eMenuArrow,
+ eMenuDownScrollArrow,
+ eMenuUpScrollArrow
+ };
+
+ enum class CheckboxOrRadioState : uint8_t { eOff, eOn, eIndeterminate };
+
+ enum class ButtonType : uint8_t {
+ eRegularPushButton,
+ eDefaultPushButton,
+ eSquareBezelPushButton,
+ eArrowButton,
+ eHelpButton,
+ eTreeTwistyPointingRight,
+ eTreeTwistyPointingDown,
+ eDisclosureButtonClosed,
+ eDisclosureButtonOpen
+ };
+
+ enum class SpinButton : uint8_t { eUp, eDown };
+
+ enum class SegmentType : uint8_t { eToolbarButton, eTab };
+
+ enum class OptimumState : uint8_t { eOptimum, eSubOptimum, eSubSubOptimum };
+
+ struct ControlParams {
+ ControlParams()
+ : disabled(false), insideActiveWindow(false), pressed(false), focused(false), rtl(false) {}
+
+ bool disabled : 1;
+ bool insideActiveWindow : 1;
+ bool pressed : 1;
+ bool focused : 1;
+ bool rtl : 1;
+ };
+
+ struct MenuIconParams {
+ MenuIcon icon = MenuIcon::eCheckmark;
+ bool disabled = false;
+ bool insideActiveMenuItem = false;
+ bool centerHorizontally = false;
+ bool rtl = false;
+ };
+
+ struct MenuItemParams {
+ bool checked = false;
+ bool disabled = false;
+ bool selected = false;
+ bool rtl = false;
+ };
+
+ struct CheckboxOrRadioParams {
+ ControlParams controlParams;
+ CheckboxOrRadioState state = CheckboxOrRadioState::eOff;
+ float verticalAlignFactor = 0.5f;
+ };
+
+ struct ButtonParams {
+ ControlParams controlParams;
+ ButtonType button = ButtonType::eRegularPushButton;
+ };
+
+ struct DropdownParams {
+ ControlParams controlParams;
+ bool pullsDown = false;
+ bool editable = false;
+ };
+
+ struct SpinButtonParams {
+ mozilla::Maybe<SpinButton> pressedButton;
+ bool disabled = false;
+ bool insideActiveWindow = false;
+ };
+
+ struct SegmentParams {
+ SegmentType segmentType = SegmentType::eToolbarButton;
+ bool insideActiveWindow = false;
+ bool pressed = false;
+ bool selected = false;
+ bool focused = false;
+ bool atLeftEnd = false;
+ bool atRightEnd = false;
+ bool drawsLeftSeparator = false;
+ bool drawsRightSeparator = false;
+ bool rtl = false;
+ };
+
+ struct TextFieldParams {
+ float verticalAlignFactor = 0.5f;
+ bool insideToolbar = false;
+ bool disabled = false;
+ bool focused = false;
+ bool rtl = false;
+ };
+
+ struct ProgressParams {
+ double value = 0.0;
+ double max = 0.0;
+ float verticalAlignFactor = 0.5f;
+ bool insideActiveWindow = false;
+ bool indeterminate = false;
+ bool horizontal = false;
+ bool rtl = false;
+ };
+
+ struct MeterParams {
+ double value = 0;
+ double min = 0;
+ double max = 0;
+ OptimumState optimumState = OptimumState::eOptimum;
+ float verticalAlignFactor = 0.5f;
+ bool horizontal = true;
+ bool rtl = false;
+ };
+
+ struct TreeHeaderCellParams {
+ ControlParams controlParams;
+ TreeSortDirection sortDirection = eTreeSortDirection_Natural;
+ bool lastTreeHeaderCell = false;
+ };
+
+ struct ScaleParams {
+ int32_t value = 0;
+ int32_t min = 0;
+ int32_t max = 0;
+ bool insideActiveWindow = false;
+ bool disabled = false;
+ bool focused = false;
+ bool horizontal = true;
+ bool reverse = false;
+ };
+
+ enum Widget : uint8_t {
+ eColorFill, // mozilla::gfx::sRGBColor
+ eMenuIcon, // MenuIconParams
+ eMenuItem, // MenuItemParams
+ eMenuSeparator, // MenuItemParams
+ eCheckbox, // CheckboxOrRadioParams
+ eRadio, // CheckboxOrRadioParams
+ eButton, // ButtonParams
+ eDropdown, // DropdownParams
+ eSpinButtons, // SpinButtonParams
+ eSpinButtonUp, // SpinButtonParams
+ eSpinButtonDown, // SpinButtonParams
+ eSegment, // SegmentParams
+ eSeparator,
+ eToolbar, // bool
+ eStatusBar, // bool
+ eGroupBox,
+ eTextField, // TextFieldParams
+ eSearchField, // TextFieldParams
+ eProgressBar, // ProgressParams
+ eMeter, // MeterParams
+ eTreeHeaderCell, // TreeHeaderCellParams
+ eScale, // ScaleParams
+ eMultilineTextField, // bool
+ eListBox,
+ eActiveSourceListSelection, // bool
+ eInactiveSourceListSelection, // bool
+ eTabPanel,
+ };
+
+ struct WidgetInfo {
+ static WidgetInfo ColorFill(const mozilla::gfx::sRGBColor& aParams) {
+ return WidgetInfo(Widget::eColorFill, aParams);
+ }
+ static WidgetInfo MenuIcon(const MenuIconParams& aParams) {
+ return WidgetInfo(Widget::eMenuIcon, aParams);
+ }
+ static WidgetInfo MenuItem(const MenuItemParams& aParams) {
+ return WidgetInfo(Widget::eMenuItem, aParams);
+ }
+ static WidgetInfo MenuSeparator(const MenuItemParams& aParams) {
+ return WidgetInfo(Widget::eMenuSeparator, aParams);
+ }
+ static WidgetInfo Checkbox(const CheckboxOrRadioParams& aParams) {
+ return WidgetInfo(Widget::eCheckbox, aParams);
+ }
+ static WidgetInfo Radio(const CheckboxOrRadioParams& aParams) {
+ return WidgetInfo(Widget::eRadio, aParams);
+ }
+ static WidgetInfo Button(const ButtonParams& aParams) {
+ return WidgetInfo(Widget::eButton, aParams);
+ }
+ static WidgetInfo Dropdown(const DropdownParams& aParams) {
+ return WidgetInfo(Widget::eDropdown, aParams);
+ }
+ static WidgetInfo SpinButtons(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtons, aParams);
+ }
+ static WidgetInfo SpinButtonUp(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtonUp, aParams);
+ }
+ static WidgetInfo SpinButtonDown(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtonDown, aParams);
+ }
+ static WidgetInfo Segment(const SegmentParams& aParams) {
+ return WidgetInfo(Widget::eSegment, aParams);
+ }
+ static WidgetInfo Separator() { return WidgetInfo(Widget::eSeparator, false); }
+ static WidgetInfo Toolbar(bool aParams) { return WidgetInfo(Widget::eToolbar, aParams); }
+ static WidgetInfo StatusBar(bool aParams) { return WidgetInfo(Widget::eStatusBar, aParams); }
+ static WidgetInfo GroupBox() { return WidgetInfo(Widget::eGroupBox, false); }
+ static WidgetInfo TextField(const TextFieldParams& aParams) {
+ return WidgetInfo(Widget::eTextField, aParams);
+ }
+ static WidgetInfo SearchField(const TextFieldParams& aParams) {
+ return WidgetInfo(Widget::eSearchField, aParams);
+ }
+ static WidgetInfo ProgressBar(const ProgressParams& aParams) {
+ return WidgetInfo(Widget::eProgressBar, aParams);
+ }
+ static WidgetInfo Meter(const MeterParams& aParams) {
+ return WidgetInfo(Widget::eMeter, aParams);
+ }
+ static WidgetInfo TreeHeaderCell(const TreeHeaderCellParams& aParams) {
+ return WidgetInfo(Widget::eTreeHeaderCell, aParams);
+ }
+ static WidgetInfo Scale(const ScaleParams& aParams) {
+ return WidgetInfo(Widget::eScale, aParams);
+ }
+ static WidgetInfo MultilineTextField(bool aParams) {
+ return WidgetInfo(Widget::eMultilineTextField, aParams);
+ }
+ static WidgetInfo ListBox() { return WidgetInfo(Widget::eListBox, false); }
+ static WidgetInfo ActiveSourceListSelection(bool aParams) {
+ return WidgetInfo(Widget::eActiveSourceListSelection, aParams);
+ }
+ static WidgetInfo InactiveSourceListSelection(bool aParams) {
+ return WidgetInfo(Widget::eInactiveSourceListSelection, aParams);
+ }
+ static WidgetInfo TabPanel(bool aParams) { return WidgetInfo(Widget::eTabPanel, aParams); }
+
+ template <typename T>
+ T Params() const {
+ MOZ_RELEASE_ASSERT(mVariant.is<T>());
+ return mVariant.as<T>();
+ }
+
+ enum Widget Widget() const { return mWidget; }
+
+ private:
+ template <typename T>
+ WidgetInfo(enum Widget aWidget, const T& aParams) : mVariant(aParams), mWidget(aWidget) {}
+
+ mozilla::Variant<mozilla::gfx::sRGBColor, MenuIconParams, MenuItemParams, CheckboxOrRadioParams,
+ ButtonParams, DropdownParams, SpinButtonParams, SegmentParams, TextFieldParams,
+ ProgressParams, MeterParams, TreeHeaderCellParams, ScaleParams, bool>
+ mVariant;
+
+ enum Widget mWidget;
+ };
+
+ explicit nsNativeThemeCocoa();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // The nsITheme interface.
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect,
+ const nsRect& aDirtyRect, DrawOverflow) override;
+ bool CreateWebRenderCommandsForWidget(mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsIFrame* aFrame, StyleAppearance aAppearance,
+ const nsRect& aRect) override;
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+
+ virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance, nsRect* aOverflowRect) override;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*, nsIFrame*, StyleAppearance) override;
+ NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance, nsAtom* aAttribute,
+ bool* aShouldRepaint, const nsAttrValue* aOldValue) override;
+ NS_IMETHOD ThemeChanged() override;
+ bool ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+ bool WidgetIsContainer(StyleAppearance aAppearance) override;
+ bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override;
+ bool ThemeNeedsComboboxDropmarker() override;
+ virtual bool WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) override;
+ virtual ThemeGeometryType ThemeGeometryTypeForWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+ virtual Transparency GetWidgetTransparency(nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+ mozilla::Maybe<WidgetInfo> ComputeWidgetInfo(nsIFrame* aFrame, StyleAppearance aAppearance,
+ const nsRect& aRect);
+ void DrawProgress(CGContextRef context, const HIRect& inBoxRect, const ProgressParams& aParams);
+
+ protected:
+ virtual ~nsNativeThemeCocoa();
+
+ LayoutDeviceIntMargin DirectionAwareMargin(const LayoutDeviceIntMargin& aMargin,
+ nsIFrame* aFrame);
+ nsIFrame* SeparatorResponsibility(nsIFrame* aBefore, nsIFrame* aAfter);
+ ControlParams ComputeControlParams(nsIFrame* aFrame, mozilla::dom::ElementState aEventState);
+ MenuIconParams ComputeMenuIconParams(nsIFrame* aParams, mozilla::dom::ElementState aEventState,
+ MenuIcon aIcon);
+ MenuItemParams ComputeMenuItemParams(nsIFrame* aFrame, mozilla::dom::ElementState aEventState,
+ bool aIsChecked);
+ SegmentParams ComputeSegmentParams(nsIFrame* aFrame, mozilla::dom::ElementState aEventState,
+ SegmentType aSegmentType);
+ TextFieldParams ComputeTextFieldParams(nsIFrame* aFrame, mozilla::dom::ElementState aEventState);
+ ProgressParams ComputeProgressParams(nsIFrame* aFrame, mozilla::dom::ElementState aEventState,
+ bool aIsHorizontal);
+ MeterParams ComputeMeterParams(nsIFrame* aFrame);
+ TreeHeaderCellParams ComputeTreeHeaderCellParams(nsIFrame* aFrame,
+ mozilla::dom::ElementState aEventState);
+ mozilla::Maybe<ScaleParams> ComputeHTMLScaleParams(nsIFrame* aFrame,
+ mozilla::dom::ElementState aEventState);
+
+ // HITheme drawing routines
+ void DrawMeter(CGContextRef context, const HIRect& inBoxRect, const MeterParams& aParams);
+ void DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect, const SegmentParams& aParams);
+ void DrawSegmentBackground(CGContextRef cgContext, const HIRect& inBoxRect,
+ const SegmentParams& aParams);
+ void DrawTabPanel(CGContextRef context, const HIRect& inBoxRect, bool aIsInsideActiveWindow);
+ void DrawScale(CGContextRef context, const HIRect& inBoxRect, const ScaleParams& aParams);
+ void DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox, const HIRect& inBoxRect,
+ const CheckboxOrRadioParams& aParams);
+ void DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams);
+ void DrawTextField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams);
+ void DrawPushButton(CGContextRef cgContext, const HIRect& inBoxRect, ButtonType aButtonType,
+ ControlParams aControlParams);
+ void DrawSquareBezelPushButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams);
+ void DrawHelpButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams);
+ void DrawDisclosureButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams, NSControlStateValue aState);
+ NSString* GetMenuIconName(const MenuIconParams& aParams);
+ NSSize GetMenuIconSize(MenuIcon aIcon);
+ void DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect, const MenuIconParams& aParams);
+ void DrawMenuItem(CGContextRef cgContext, const CGRect& inBoxRect, const MenuItemParams& aParams);
+ void DrawMenuSeparator(CGContextRef cgContext, const CGRect& inBoxRect,
+ const MenuItemParams& aParams);
+ void DrawHIThemeButton(CGContextRef cgContext, const HIRect& aRect, ThemeButtonKind aKind,
+ ThemeButtonValue aValue, ThemeDrawState aState,
+ ThemeButtonAdornment aAdornment, const ControlParams& aParams);
+ void DrawButton(CGContextRef context, const HIRect& inBoxRect, const ButtonParams& aParams);
+ void DrawTreeHeaderCell(CGContextRef context, const HIRect& inBoxRect,
+ const TreeHeaderCellParams& aParams);
+ void DrawDropdown(CGContextRef context, const HIRect& inBoxRect, const DropdownParams& aParams);
+ HIThemeButtonDrawInfo SpinButtonDrawInfo(ThemeButtonKind aKind, const SpinButtonParams& aParams);
+ void DrawSpinButtons(CGContextRef context, const HIRect& inBoxRect,
+ const SpinButtonParams& aParams);
+ void DrawSpinButton(CGContextRef context, const HIRect& inBoxRect, SpinButton aDrawnButton,
+ const SpinButtonParams& aParams);
+ void DrawToolbar(CGContextRef cgContext, const CGRect& inBoxRect, bool aIsMain);
+ void DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect, bool aIsMain);
+ void DrawMultilineTextField(CGContextRef cgContext, const CGRect& inBoxRect, bool aIsFocused);
+ void DrawSourceListSelection(CGContextRef aContext, const CGRect& aRect, bool aWindowIsActive,
+ bool aSelectionIsActive);
+
+ void RenderWidget(const WidgetInfo& aWidgetInfo, mozilla::ColorScheme,
+ mozilla::gfx::DrawTarget& aDrawTarget, const mozilla::gfx::Rect& aWidgetRect,
+ const mozilla::gfx::Rect& aDirtyRect, float aScale);
+
+ private:
+ NSButtonCell* mDisclosureButtonCell;
+ NSButtonCell* mHelpButtonCell;
+ NSButtonCell* mPushButtonCell;
+ NSButtonCell* mRadioButtonCell;
+ NSButtonCell* mCheckboxCell;
+ NSTextFieldCell* mTextFieldCell;
+ MOZSearchFieldCell* mSearchFieldCell;
+ NSPopUpButtonCell* mDropdownCell;
+ NSComboBoxCell* mComboBoxCell;
+ NSProgressBarCell* mProgressBarCell;
+ NSLevelIndicatorCell* mMeterBarCell;
+ NSTableHeaderCell* mTreeHeaderCell;
+ MOZCellDrawWindow* mCellDrawWindow = nil;
+ MOZCellDrawView* mCellDrawView;
+};
+
+#endif // nsNativeThemeCocoa_h_
diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm
new file mode 100644
index 0000000000..e14f161663
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeCocoa.mm
@@ -0,0 +1,3444 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsNativeThemeCocoa.h"
+#include <objc/NSObjCRuntime.h>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "nsChildView.h"
+#include "nsDeviceContext.h"
+#include "nsLayoutUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsNumberControlFrame.h"
+#include "nsRangeFrame.h"
+#include "nsRect.h"
+#include "nsSize.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFrame.h"
+#include "nsAtom.h"
+#include "nsNameSpaceManager.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaWindow.h"
+#include "nsNativeThemeColors.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Range.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLMeterElement.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsLookAndFeel.h"
+#include "MacThemeGeometryType.h"
+#include "SDKDeclarations.h"
+#include "VibrancyManager.h"
+
+#include "gfxContext.h"
+#include "gfxQuartzSurface.h"
+#include "gfxQuartzNativeDrawing.h"
+#include "gfxUtils.h" // for ToDeviceColor
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using mozilla::dom::HTMLMeterElement;
+
+#define DRAW_IN_FRAME_DEBUG 0
+#define SCROLLBARS_VISUAL_DEBUG 0
+
+// private Quartz routines needed here
+extern "C" {
+CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform);
+CG_EXTERN void CGContextSetBaseCTM(CGContextRef, CGAffineTransform);
+typedef CFTypeRef CUIRendererRef;
+void CUIDraw(CUIRendererRef r, CGRect rect, CGContextRef ctx, CFDictionaryRef options,
+ CFDictionaryRef* result);
+}
+
+static bool IsDarkAppearance(NSAppearance* appearance) {
+ if (@available(macOS 10.14, *)) {
+ return [appearance.name isEqualToString:NSAppearanceNameDarkAqua];
+ }
+ return false;
+}
+
+// Workaround for NSCell control tint drawing
+// Without this workaround, NSCells are always drawn with the clear control tint
+// as long as they're not attached to an NSControl which is a subview of an active window.
+// XXXmstange Why doesn't Webkit need this?
+@implementation NSCell (ControlTintWorkaround)
+- (int)_realControlTint {
+ return [self controlTint];
+}
+@end
+
+// This is the window for our MOZCellDrawView. When an NSCell is drawn, some NSCell implementations
+// look at the draw view's window to determine whether the cell should draw with the active look.
+@interface MOZCellDrawWindow : NSWindow
+@property BOOL cellsShouldLookActive;
+@end
+
+@implementation MOZCellDrawWindow
+
+// Override three different methods, for good measure. The NSCell implementation could call any one
+// of them.
+- (BOOL)_hasActiveAppearance {
+ return self.cellsShouldLookActive;
+}
+- (BOOL)hasKeyAppearance {
+ return self.cellsShouldLookActive;
+}
+- (BOOL)_hasKeyAppearance {
+ return self.cellsShouldLookActive;
+}
+
+@end
+
+// The purpose of this class is to provide objects that can be used when drawing
+// NSCells using drawWithFrame:inView: without causing any harm. Only a small
+// number of methods are called on the draw view, among those "isFlipped" and
+// "currentEditor": isFlipped needs to return YES in order to avoid drawing bugs
+// on 10.4 (see bug 465069); currentEditor (which isn't even a method of
+// NSView) will be called when drawing search fields, and we only provide it in
+// order to prevent "unrecognized selector" exceptions.
+// There's no need to pass the actual NSView that we're drawing into to
+// drawWithFrame:inView:. What's more, doing so even causes unnecessary
+// invalidations as soon as we draw a focusring!
+// This class needs to be an NSControl so that NSTextFieldCell (and
+// NSSearchFieldCell, which is a subclass of NSTextFieldCell) draws a focus ring.
+@interface MOZCellDrawView : NSControl
+// Called by NSTreeHeaderCell during drawing.
+@property BOOL _drawingEndSeparator;
+@end
+
+@implementation MOZCellDrawView
+
+- (BOOL)isFlipped {
+ return YES;
+}
+
+- (NSText*)currentEditor {
+ return nil;
+}
+
+@end
+
+static void DrawFocusRingForCellIfNeeded(NSCell* aCell, NSRect aWithFrame, NSView* aInView) {
+ if ([aCell showsFirstResponder]) {
+ CGContextRef cgContext = [[NSGraphicsContext currentContext] CGContext];
+ CGContextSaveGState(cgContext);
+
+ // It's important to set the focus ring style before we enter the
+ // transparency layer so that the transparency layer only contains
+ // the normal button mask without the focus ring, and the conversion
+ // to the focus ring shape happens only when the transparency layer is
+ // ended.
+ NSSetFocusRingStyle(NSFocusRingOnly);
+
+ // We need to draw the whole button into a transparency layer because
+ // many button types are composed of multiple parts, and if these parts
+ // were drawn while the focus ring style was active, each individual part
+ // would produce a focus ring for itself. But we only want one focus ring
+ // for the whole button. The transparency layer is a way to merge the
+ // individual button parts together before the focus ring shape is
+ // calculated.
+ CGContextBeginTransparencyLayerWithRect(cgContext, NSRectToCGRect(aWithFrame), 0);
+ [aCell drawFocusRingMaskWithFrame:aWithFrame inView:aInView];
+ CGContextEndTransparencyLayer(cgContext);
+
+ CGContextRestoreGState(cgContext);
+ }
+}
+
+static void DrawCellIncludingFocusRing(NSCell* aCell, NSRect aWithFrame, NSView* aInView) {
+ [aCell drawWithFrame:aWithFrame inView:aInView];
+ DrawFocusRingForCellIfNeeded(aCell, aWithFrame, aInView);
+}
+
+/**
+ * NSProgressBarCell is used to draw progress bars of any size.
+ */
+@interface NSProgressBarCell : NSCell {
+ /*All instance variables are private*/
+ double mValue;
+ double mMax;
+ bool mIsIndeterminate;
+ bool mIsHorizontal;
+}
+
+- (void)setValue:(double)value;
+- (double)value;
+- (void)setMax:(double)max;
+- (double)max;
+- (void)setIndeterminate:(bool)aIndeterminate;
+- (bool)isIndeterminate;
+- (void)setHorizontal:(bool)aIsHorizontal;
+- (bool)isHorizontal;
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView;
+@end
+
+@implementation NSProgressBarCell
+
+- (void)setMax:(double)aMax {
+ mMax = aMax;
+}
+
+- (double)max {
+ return mMax;
+}
+
+- (void)setValue:(double)aValue {
+ mValue = aValue;
+}
+
+- (double)value {
+ return mValue;
+}
+
+- (void)setIndeterminate:(bool)aIndeterminate {
+ mIsIndeterminate = aIndeterminate;
+}
+
+- (bool)isIndeterminate {
+ return mIsIndeterminate;
+}
+
+- (void)setHorizontal:(bool)aIsHorizontal {
+ mIsHorizontal = aIsHorizontal;
+}
+
+- (bool)isHorizontal {
+ return mIsHorizontal;
+}
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ CGContext* cgContext = [[NSGraphicsContext currentContext] CGContext];
+
+ HIThemeTrackDrawInfo tdi;
+
+ tdi.version = 0;
+ tdi.min = 0;
+
+ tdi.value = INT32_MAX * (mValue / mMax);
+ tdi.max = INT32_MAX;
+ tdi.bounds = NSRectToCGRect(cellFrame);
+ tdi.attributes = mIsHorizontal ? kThemeTrackHorizontal : 0;
+ tdi.enableState =
+ [self controlTint] == NSClearControlTint ? kThemeTrackInactive : kThemeTrackActive;
+
+ NSControlSize size = [self controlSize];
+ if (size == NSControlSizeRegular) {
+ tdi.kind = mIsIndeterminate ? kThemeLargeIndeterminateBar : kThemeLargeProgressBar;
+ } else {
+ NS_ASSERTION(size == NSControlSizeSmall,
+ "We shouldn't have another size than small and regular for the moment");
+ tdi.kind = mIsIndeterminate ? kThemeMediumIndeterminateBar : kThemeMediumProgressBar;
+ }
+
+ int32_t stepsPerSecond = mIsIndeterminate ? 60 : 30;
+ int32_t milliSecondsPerStep = 1000 / stepsPerSecond;
+ tdi.trackInfo.progress.phase =
+ uint8_t(PR_IntervalToMilliseconds(PR_IntervalNow()) / milliSecondsPerStep);
+
+ HIThemeDrawTrack(&tdi, NULL, cgContext, kHIThemeOrientationNormal);
+}
+
+@end
+
+@interface MOZSearchFieldCell : NSSearchFieldCell
+@property BOOL shouldUseToolbarStyle;
+@end
+
+@implementation MOZSearchFieldCell
+
+- (instancetype)init {
+ // We would like to render a search field which has the magnifying glass icon at the start of the
+ // search field, and no cancel button.
+ // On 10.12 and 10.13, empty search fields render the magnifying glass icon in the middle of the
+ // field. So in order to get the icon to show at the start of the field, we need to give the field
+ // some content. We achieve this with a single space character.
+ self = [super initTextCell:@" "];
+
+ // However, because the field is now non-empty, by default it shows a cancel button. To hide the
+ // cancel button, override it with a custom NSButtonCell which renders nothing.
+ NSButtonCell* invisibleCell = [[NSButtonCell alloc] initImageCell:nil];
+ invisibleCell.bezeled = NO;
+ invisibleCell.bordered = NO;
+ self.cancelButtonCell = invisibleCell;
+ [invisibleCell release];
+
+ return self;
+}
+
+- (BOOL)_isToolbarMode {
+ return self.shouldUseToolbarStyle;
+}
+
+@end
+
+#define HITHEME_ORIENTATION kHIThemeOrientationNormal
+
+static CGFloat kMaxFocusRingWidth = 0; // initialized by the nsNativeThemeCocoa constructor
+
+// These enums are for indexing into the margin array.
+enum {
+ leopardOSorlater = 0, // 10.6 - 10.9
+ yosemiteOSorlater = 1 // 10.10+
+};
+
+enum { miniControlSize, smallControlSize, regularControlSize };
+
+enum { leftMargin, topMargin, rightMargin, bottomMargin };
+
+static size_t EnumSizeForCocoaSize(NSControlSize cocoaControlSize) {
+ if (cocoaControlSize == NSControlSizeMini)
+ return miniControlSize;
+ else if (cocoaControlSize == NSControlSizeSmall)
+ return smallControlSize;
+ else
+ return regularControlSize;
+}
+
+static NSControlSize CocoaSizeForEnum(int32_t enumControlSize) {
+ if (enumControlSize == miniControlSize)
+ return NSControlSizeMini;
+ else if (enumControlSize == smallControlSize)
+ return NSControlSizeSmall;
+ else
+ return NSControlSizeRegular;
+}
+
+static NSString* CUIControlSizeForCocoaSize(NSControlSize aControlSize) {
+ if (aControlSize == NSControlSizeRegular)
+ return @"regular";
+ else if (aControlSize == NSControlSizeSmall)
+ return @"small";
+ else
+ return @"mini";
+}
+
+static void InflateControlRect(NSRect* rect, NSControlSize cocoaControlSize,
+ const float marginSet[][3][4]) {
+ if (!marginSet) return;
+
+ static int osIndex = yosemiteOSorlater;
+ size_t controlSize = EnumSizeForCocoaSize(cocoaControlSize);
+ const float* buttonMargins = marginSet[osIndex][controlSize];
+ rect->origin.x -= buttonMargins[leftMargin];
+ rect->origin.y -= buttonMargins[bottomMargin];
+ rect->size.width += buttonMargins[leftMargin] + buttonMargins[rightMargin];
+ rect->size.height += buttonMargins[bottomMargin] + buttonMargins[topMargin];
+}
+
+static NSWindow* NativeWindowForFrame(nsIFrame* aFrame, nsIWidget** aTopLevelWidget = NULL) {
+ if (!aFrame) return nil;
+
+ nsIWidget* widget = aFrame->GetNearestWidget();
+ if (!widget) return nil;
+
+ nsIWidget* topLevelWidget = widget->GetTopLevelWidget();
+ if (aTopLevelWidget) *aTopLevelWidget = topLevelWidget;
+
+ return (NSWindow*)topLevelWidget->GetNativeData(NS_NATIVE_WINDOW);
+}
+
+static NSSize WindowButtonsSize(nsIFrame* aFrame) {
+ NSWindow* window = NativeWindowForFrame(aFrame);
+ if (!window) {
+ // Return fallback values.
+ return NSMakeSize(54, 16);
+ }
+
+ NSRect buttonBox = NSZeroRect;
+ NSButton* closeButton = [window standardWindowButton:NSWindowCloseButton];
+ if (closeButton) {
+ buttonBox = NSUnionRect(buttonBox, [closeButton frame]);
+ }
+ NSButton* minimizeButton = [window standardWindowButton:NSWindowMiniaturizeButton];
+ if (minimizeButton) {
+ buttonBox = NSUnionRect(buttonBox, [minimizeButton frame]);
+ }
+ NSButton* zoomButton = [window standardWindowButton:NSWindowZoomButton];
+ if (zoomButton) {
+ buttonBox = NSUnionRect(buttonBox, [zoomButton frame]);
+ }
+ return buttonBox.size;
+}
+
+static BOOL FrameIsInActiveWindow(nsIFrame* aFrame) {
+ nsIWidget* topLevelWidget = NULL;
+ NSWindow* win = NativeWindowForFrame(aFrame, &topLevelWidget);
+ if (!topLevelWidget || !win) return YES;
+
+ // XUL popups, e.g. the toolbar customization popup, can't become key windows,
+ // but controls in these windows should still get the active look.
+ if (topLevelWidget->GetWindowType() == widget::WindowType::Popup) {
+ return YES;
+ }
+ if ([win isSheet]) {
+ return [win isKeyWindow];
+ }
+ return [win isMainWindow] && ![win attachedSheet];
+}
+
+// Toolbar controls and content controls respond to different window
+// activeness states.
+static BOOL IsActive(nsIFrame* aFrame, BOOL aIsToolbarControl) {
+ if (aIsToolbarControl) return [NativeWindowForFrame(aFrame) isMainWindow];
+ return FrameIsInActiveWindow(aFrame);
+}
+
+static bool IsInSourceList(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame->GetParent(); frame;
+ frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) {
+ if (frame->StyleDisplay()->EffectiveAppearance() == StyleAppearance::MozMacSourceList) {
+ return true;
+ }
+ }
+ return false;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeCocoa, nsNativeTheme, nsITheme)
+
+nsNativeThemeCocoa::nsNativeThemeCocoa() : ThemeCocoa(ScrollbarStyle()) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ kMaxFocusRingWidth = 7;
+
+ // provide a local autorelease pool, as this is called during startup
+ // before the main event-loop pool is in place
+ nsAutoreleasePool pool;
+
+ mDisclosureButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mDisclosureButtonCell setBezelStyle:NSRoundedDisclosureBezelStyle];
+ [mDisclosureButtonCell setButtonType:NSPushOnPushOffButton];
+ [mDisclosureButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mHelpButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mHelpButtonCell setBezelStyle:NSHelpButtonBezelStyle];
+ [mHelpButtonCell setButtonType:NSMomentaryPushInButton];
+ [mHelpButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mPushButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mPushButtonCell setButtonType:NSMomentaryPushInButton];
+ [mPushButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mRadioButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mRadioButtonCell setButtonType:NSRadioButton];
+
+ mCheckboxCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mCheckboxCell setButtonType:NSSwitchButton];
+ [mCheckboxCell setAllowsMixedState:YES];
+
+ mTextFieldCell = [[NSTextFieldCell alloc] initTextCell:@""];
+ [mTextFieldCell setBezeled:YES];
+ [mTextFieldCell setEditable:YES];
+ [mTextFieldCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mSearchFieldCell = [[MOZSearchFieldCell alloc] init];
+ [mSearchFieldCell setBezelStyle:NSTextFieldRoundedBezel];
+ [mSearchFieldCell setBezeled:YES];
+ [mSearchFieldCell setEditable:YES];
+ [mSearchFieldCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mDropdownCell = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO];
+
+ mComboBoxCell = [[NSComboBoxCell alloc] initTextCell:@""];
+ [mComboBoxCell setBezeled:YES];
+ [mComboBoxCell setEditable:YES];
+ [mComboBoxCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mProgressBarCell = [[NSProgressBarCell alloc] init];
+
+ mMeterBarCell = [[NSLevelIndicatorCell alloc]
+ initWithLevelIndicatorStyle:NSContinuousCapacityLevelIndicatorStyle];
+
+ mTreeHeaderCell = [[NSTableHeaderCell alloc] init];
+
+ mCellDrawView = [[MOZCellDrawView alloc] init];
+
+ if (XRE_IsParentProcess()) {
+ // Put the cell draw view into a window that is never shown.
+ // This allows us to convince some NSCell implementations (such as NSButtonCell for default
+ // buttons) to draw with the active appearance. Another benefit of putting the draw view in a
+ // window is the fact that it lets NSTextFieldCell (and its subclass NSSearchFieldCell) inherit
+ // the current NSApplication effectiveAppearance automatically, so the field adapts to Dark Mode
+ // correctly.
+ // We don't create this window when the native theme is used in the content process because
+ // NSWindow creation runs into the sandbox and because we never run default buttons in content
+ // processes anyway.
+ mCellDrawWindow = [[MOZCellDrawWindow alloc] initWithContentRect:NSZeroRect
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ [mCellDrawWindow.contentView addSubview:mCellDrawView];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsNativeThemeCocoa::~nsNativeThemeCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mMeterBarCell release];
+ [mProgressBarCell release];
+ [mDisclosureButtonCell release];
+ [mHelpButtonCell release];
+ [mPushButtonCell release];
+ [mRadioButtonCell release];
+ [mCheckboxCell release];
+ [mTextFieldCell release];
+ [mSearchFieldCell release];
+ [mDropdownCell release];
+ [mComboBoxCell release];
+ [mTreeHeaderCell release];
+ [mCellDrawWindow release];
+ [mCellDrawView release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Limit on the area of the target rect (in pixels^2) in
+// DrawCellWithScaling() and DrawButton() and above which we
+// don't draw the object into a bitmap buffer. This is to avoid crashes in
+// [NSGraphicsContext graphicsContextWithCGContext:flipped:] and
+// CGContextDrawImage(), and also to avoid very poor drawing performance in
+// CGContextDrawImage() when it scales the bitmap (particularly if xscale or
+// yscale is less than but near 1 -- e.g. 0.9). This value was determined
+// by trial and error, on OS X 10.4.11 and 10.5.4, and on systems with
+// different amounts of RAM.
+#define BITMAP_MAX_AREA 500000
+
+static int GetBackingScaleFactorForRendering(CGContextRef cgContext) {
+ CGAffineTransform ctm = CGContextGetUserSpaceToDeviceSpaceTransform(cgContext);
+ CGRect transformedUserSpacePixel = CGRectApplyAffineTransform(CGRectMake(0, 0, 1, 1), ctm);
+ float maxScale = std::max(fabs(transformedUserSpacePixel.size.width),
+ fabs(transformedUserSpacePixel.size.height));
+ return maxScale > 1.0 ? 2 : 1;
+}
+
+/*
+ * Draw the given NSCell into the given cgContext.
+ *
+ * destRect - the size and position of the resulting control rectangle
+ * controlSize - the NSControlSize which will be given to the NSCell before
+ * asking it to render
+ * naturalSize - The natural dimensions of this control.
+ * If the control rect size is not equal to either of these, a scale
+ * will be applied to the context so that rendering the control at the
+ * natural size will result in it filling the destRect space.
+ * If a control has no natural dimensions in either/both axes, pass 0.0f.
+ * minimumSize - The minimum dimensions of this control.
+ * If the control rect size is less than the minimum for a given axis,
+ * a scale will be applied to the context so that the minimum is used
+ * for drawing. If a control has no minimum dimensions in either/both
+ * axes, pass 0.0f.
+ * marginSet - an array of margins; a multidimensional array of [2][3][4],
+ * with the first dimension being the OS version (Tiger or Leopard),
+ * the second being the control size (mini, small, regular), and the third
+ * being the 4 margin values (left, top, right, bottom).
+ * view - The NSView that we're drawing into. As far as I can tell, it doesn't
+ * matter if this is really the right view; it just has to return YES when
+ * asked for isFlipped. Otherwise we'll get drawing bugs on 10.4.
+ * mirrorHorizontal - whether to mirror the cell horizontally
+ */
+static void DrawCellWithScaling(NSCell* cell, CGContextRef cgContext, const HIRect& destRect,
+ NSControlSize controlSize, NSSize naturalSize, NSSize minimumSize,
+ const float marginSet[][3][4], NSView* view,
+ BOOL mirrorHorizontal) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSRect drawRect =
+ NSMakeRect(destRect.origin.x, destRect.origin.y, destRect.size.width, destRect.size.height);
+
+ if (naturalSize.width != 0.0f) drawRect.size.width = naturalSize.width;
+ if (naturalSize.height != 0.0f) drawRect.size.height = naturalSize.height;
+
+ // Keep aspect ratio when scaling if one dimension is free.
+ if (naturalSize.width == 0.0f && naturalSize.height != 0.0f)
+ drawRect.size.width = destRect.size.width * naturalSize.height / destRect.size.height;
+ if (naturalSize.height == 0.0f && naturalSize.width != 0.0f)
+ drawRect.size.height = destRect.size.height * naturalSize.width / destRect.size.width;
+
+ // Honor minimum sizes.
+ if (drawRect.size.width < minimumSize.width) drawRect.size.width = minimumSize.width;
+ if (drawRect.size.height < minimumSize.height) drawRect.size.height = minimumSize.height;
+
+ [NSGraphicsContext saveGraphicsState];
+
+ // Only skip the buffer if the area of our cell (in pixels^2) is too large.
+ if (drawRect.size.width * drawRect.size.height > BITMAP_MAX_AREA) {
+ // Inflate the rect Gecko gave us by the margin for the control.
+ InflateControlRect(&drawRect, controlSize, marginSet);
+
+ NSGraphicsContext* savedContext = [NSGraphicsContext currentContext];
+ [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:cgContext
+ flipped:YES]];
+
+ DrawCellIncludingFocusRing(cell, drawRect, view);
+
+ [NSGraphicsContext setCurrentContext:savedContext];
+ } else {
+ float w = ceil(drawRect.size.width);
+ float h = ceil(drawRect.size.height);
+ NSRect tmpRect = NSMakeRect(kMaxFocusRingWidth, kMaxFocusRingWidth, w, h);
+
+ // inflate to figure out the frame we need to tell NSCell to draw in, to get something that's
+ // 0,0,w,h
+ InflateControlRect(&tmpRect, controlSize, marginSet);
+
+ // and then, expand by kMaxFocusRingWidth size to make sure we can capture any focus ring
+ w += kMaxFocusRingWidth * 2.0;
+ h += kMaxFocusRingWidth * 2.0;
+
+ int backingScaleFactor = GetBackingScaleFactorForRendering(cgContext);
+ CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
+ CGContextRef ctx = CGBitmapContextCreate(
+ NULL, (int)w * backingScaleFactor, (int)h * backingScaleFactor, 8,
+ (int)w * backingScaleFactor * 4, rgb, kCGImageAlphaPremultipliedFirst);
+ CGColorSpaceRelease(rgb);
+
+ // We need to flip the image twice in order to avoid drawing bugs on 10.4, see bug 465069.
+ // This is the first flip transform, applied to cgContext.
+ CGContextScaleCTM(cgContext, 1.0f, -1.0f);
+ CGContextTranslateCTM(cgContext, 0.0f, -(2.0 * destRect.origin.y + destRect.size.height));
+ if (mirrorHorizontal) {
+ CGContextScaleCTM(cgContext, -1.0f, 1.0f);
+ CGContextTranslateCTM(cgContext, -(2.0 * destRect.origin.x + destRect.size.width), 0.0f);
+ }
+
+ NSGraphicsContext* savedContext = [NSGraphicsContext currentContext];
+ [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:ctx
+ flipped:YES]];
+
+ CGContextScaleCTM(ctx, backingScaleFactor, backingScaleFactor);
+
+ // Set the context's "base transform" to in order to get correctly-sized focus rings.
+ CGContextSetBaseCTM(ctx, CGAffineTransformMakeScale(backingScaleFactor, backingScaleFactor));
+
+ // This is the second flip transform, applied to ctx.
+ CGContextScaleCTM(ctx, 1.0f, -1.0f);
+ CGContextTranslateCTM(ctx, 0.0f, -(2.0 * tmpRect.origin.y + tmpRect.size.height));
+
+ DrawCellIncludingFocusRing(cell, tmpRect, view);
+
+ [NSGraphicsContext setCurrentContext:savedContext];
+
+ CGImageRef img = CGBitmapContextCreateImage(ctx);
+
+ // Drop the image into the original destination rectangle, scaling to fit
+ // Only scale kMaxFocusRingWidth by xscale/yscale when the resulting rect
+ // doesn't extend beyond the overflow rect
+ float xscale = destRect.size.width / drawRect.size.width;
+ float yscale = destRect.size.height / drawRect.size.height;
+ float scaledFocusRingX = xscale < 1.0f ? kMaxFocusRingWidth * xscale : kMaxFocusRingWidth;
+ float scaledFocusRingY = yscale < 1.0f ? kMaxFocusRingWidth * yscale : kMaxFocusRingWidth;
+ CGContextDrawImage(
+ cgContext,
+ CGRectMake(destRect.origin.x - scaledFocusRingX, destRect.origin.y - scaledFocusRingY,
+ destRect.size.width + scaledFocusRingX * 2,
+ destRect.size.height + scaledFocusRingY * 2),
+ img);
+
+ CGImageRelease(img);
+ CGContextRelease(ctx);
+ }
+
+ [NSGraphicsContext restoreGraphicsState];
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, destRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+struct CellRenderSettings {
+ // The natural dimensions of the control.
+ // If a control has no natural dimensions in either/both axes, set to 0.0f.
+ NSSize naturalSizes[3];
+
+ // The minimum dimensions of the control.
+ // If a control has no minimum dimensions in either/both axes, set to 0.0f.
+ NSSize minimumSizes[3];
+
+ // A three-dimensional array,
+ // with the first dimension being the OS version ([0] 10.6-10.9, [1] 10.10 and above),
+ // the second being the control size (mini, small, regular), and the third
+ // being the 4 margin values (left, top, right, bottom).
+ float margins[2][3][4];
+};
+
+/*
+ * This is a helper method that returns the required NSControlSize given a size
+ * and the size of the three controls plus a tolerance.
+ * size - The width or the height of the element to draw.
+ * sizes - An array with the all the width/height of the element for its
+ * different sizes.
+ * tolerance - The tolerance as passed to DrawCellWithSnapping.
+ * NOTE: returns NSControlSizeRegular if all values in 'sizes' are zero.
+ */
+static NSControlSize FindControlSize(CGFloat size, const CGFloat* sizes, CGFloat tolerance) {
+ for (uint32_t i = miniControlSize; i <= regularControlSize; ++i) {
+ if (sizes[i] == 0) {
+ continue;
+ }
+
+ CGFloat next = 0;
+ // Find next value.
+ for (uint32_t j = i + 1; j <= regularControlSize; ++j) {
+ if (sizes[j] != 0) {
+ next = sizes[j];
+ break;
+ }
+ }
+
+ // If it's the latest value, we pick it.
+ if (next == 0) {
+ return CocoaSizeForEnum(i);
+ }
+
+ if (size <= sizes[i] + tolerance && size < next) {
+ return CocoaSizeForEnum(i);
+ }
+ }
+
+ // If we are here, that means sizes[] was an array with only empty values
+ // or the algorithm above is wrong.
+ // The former can happen but the later would be wrong.
+ NS_ASSERTION(sizes[0] == 0 && sizes[1] == 0 && sizes[2] == 0,
+ "We found no control! We shouldn't be there!");
+ return CocoaSizeForEnum(regularControlSize);
+}
+
+/*
+ * Draw the given NSCell into the given cgContext with a nice control size.
+ *
+ * This function is similar to DrawCellWithScaling, but it decides what
+ * control size to use based on the destRect's size.
+ * Scaling is only applied when the difference between the destRect's size
+ * and the next smaller natural size is greater than snapTolerance. Otherwise
+ * it snaps to the next smaller control size without scaling because unscaled
+ * controls look nicer.
+ */
+static void DrawCellWithSnapping(NSCell* cell, CGContextRef cgContext, const HIRect& destRect,
+ const CellRenderSettings settings, float verticalAlignFactor,
+ NSView* view, BOOL mirrorHorizontal, float snapTolerance = 2.0f) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ const float rectWidth = destRect.size.width, rectHeight = destRect.size.height;
+ const NSSize* sizes = settings.naturalSizes;
+ const NSSize miniSize = sizes[EnumSizeForCocoaSize(NSControlSizeMini)];
+ const NSSize smallSize = sizes[EnumSizeForCocoaSize(NSControlSizeSmall)];
+ const NSSize regularSize = sizes[EnumSizeForCocoaSize(NSControlSizeRegular)];
+
+ HIRect drawRect = destRect;
+
+ CGFloat controlWidths[3] = {miniSize.width, smallSize.width, regularSize.width};
+ NSControlSize controlSizeX = FindControlSize(rectWidth, controlWidths, snapTolerance);
+ CGFloat controlHeights[3] = {miniSize.height, smallSize.height, regularSize.height};
+ NSControlSize controlSizeY = FindControlSize(rectHeight, controlHeights, snapTolerance);
+
+ NSControlSize controlSize = NSControlSizeRegular;
+ size_t sizeIndex = 0;
+
+ // At some sizes, don't scale but snap.
+ const NSControlSize smallerControlSize =
+ EnumSizeForCocoaSize(controlSizeX) < EnumSizeForCocoaSize(controlSizeY) ? controlSizeX
+ : controlSizeY;
+ const size_t smallerControlSizeIndex = EnumSizeForCocoaSize(smallerControlSize);
+ const NSSize size = sizes[smallerControlSizeIndex];
+ float diffWidth = size.width ? rectWidth - size.width : 0.0f;
+ float diffHeight = size.height ? rectHeight - size.height : 0.0f;
+ if (diffWidth >= 0.0f && diffHeight >= 0.0f && diffWidth <= snapTolerance &&
+ diffHeight <= snapTolerance) {
+ // Snap to the smaller control size.
+ controlSize = smallerControlSize;
+ sizeIndex = smallerControlSizeIndex;
+ MOZ_ASSERT(sizeIndex < ArrayLength(settings.naturalSizes));
+
+ // Resize and center the drawRect.
+ if (sizes[sizeIndex].width) {
+ drawRect.origin.x += ceil((destRect.size.width - sizes[sizeIndex].width) / 2);
+ drawRect.size.width = sizes[sizeIndex].width;
+ }
+ if (sizes[sizeIndex].height) {
+ drawRect.origin.y +=
+ floor((destRect.size.height - sizes[sizeIndex].height) * verticalAlignFactor);
+ drawRect.size.height = sizes[sizeIndex].height;
+ }
+ } else {
+ // Use the larger control size.
+ controlSize = EnumSizeForCocoaSize(controlSizeX) > EnumSizeForCocoaSize(controlSizeY)
+ ? controlSizeX
+ : controlSizeY;
+ sizeIndex = EnumSizeForCocoaSize(controlSize);
+ }
+
+ [cell setControlSize:controlSize];
+
+ MOZ_ASSERT(sizeIndex < ArrayLength(settings.minimumSizes));
+ const NSSize minimumSize = settings.minimumSizes[sizeIndex];
+ DrawCellWithScaling(cell, cgContext, drawRect, controlSize, sizes[sizeIndex], minimumSize,
+ settings.margins, view, mirrorHorizontal);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@interface NSWindow (CoreUIRendererPrivate)
++ (CUIRendererRef)coreUIRenderer;
+@end
+
+@interface NSObject (NSAppearanceCoreUIRendering)
+- (void)_drawInRect:(CGRect)rect context:(CGContextRef)cgContext options:(id)options;
+@end
+
+static void RenderWithCoreUI(CGRect aRect, CGContextRef cgContext, NSDictionary* aOptions,
+ bool aSkipAreaCheck = false) {
+ if (!aSkipAreaCheck && aRect.size.width * aRect.size.height > BITMAP_MAX_AREA) {
+ return;
+ }
+
+ NSAppearance* appearance = NSAppearance.currentAppearance;
+ if (appearance && [appearance respondsToSelector:@selector(_drawInRect:context:options:)]) {
+ // Render through NSAppearance on Mac OS 10.10 and up. This will call
+ // CUIDraw with a CoreUI renderer that will give us the correct 10.10
+ // style. Calling CUIDraw directly with [NSWindow coreUIRenderer] still
+ // renders 10.9-style widgets on 10.10.
+ [appearance _drawInRect:aRect context:cgContext options:aOptions];
+ } else {
+ // 10.9 and below
+ CUIRendererRef renderer =
+ [NSWindow respondsToSelector:@selector(coreUIRenderer)] ? [NSWindow coreUIRenderer] : nil;
+ CUIDraw(renderer, aRect, cgContext, (CFDictionaryRef)aOptions, NULL);
+ }
+}
+
+static float VerticalAlignFactor(nsIFrame* aFrame) {
+ if (!aFrame) return 0.5f; // default: center
+
+ const auto& va = aFrame->StyleDisplay()->mVerticalAlign;
+ auto kw = va.IsKeyword() ? va.AsKeyword() : StyleVerticalAlignKeyword::Middle;
+ switch (kw) {
+ case StyleVerticalAlignKeyword::Top:
+ case StyleVerticalAlignKeyword::TextTop:
+ return 0.0f;
+
+ case StyleVerticalAlignKeyword::Sub:
+ case StyleVerticalAlignKeyword::Super:
+ case StyleVerticalAlignKeyword::Middle:
+ case StyleVerticalAlignKeyword::MozMiddleWithBaseline:
+ return 0.5f;
+
+ case StyleVerticalAlignKeyword::Baseline:
+ case StyleVerticalAlignKeyword::Bottom:
+ case StyleVerticalAlignKeyword::TextBottom:
+ return 1.0f;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid vertical-align");
+ return 0.5f;
+ }
+}
+
+static void ApplyControlParamsToNSCell(nsNativeThemeCocoa::ControlParams aControlParams,
+ NSCell* aCell) {
+ [aCell setEnabled:!aControlParams.disabled];
+ [aCell setShowsFirstResponder:(aControlParams.focused && !aControlParams.disabled &&
+ aControlParams.insideActiveWindow)];
+ [aCell setHighlighted:aControlParams.pressed];
+}
+
+// These are the sizes that Gecko needs to request to draw if it wants
+// to get a standard-sized Aqua radio button drawn. Note that the rects
+// that draw these are actually a little bigger.
+static const CellRenderSettings radioSettings = {{
+ NSMakeSize(11, 11), // mini
+ NSMakeSize(13, 13), // small
+ NSMakeSize(16, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 1, 1, 1}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 2}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+static const CellRenderSettings checkboxSettings = {{
+ NSMakeSize(11, 11), // mini
+ NSMakeSize(13, 13), // small
+ NSMakeSize(16, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 1, 0, 0}, // mini
+ {0, 1, 0, 1}, // small
+ {0, 1, 0, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 1, 0, 0}, // mini
+ {0, 1, 0, 1}, // small
+ {0, 1, 0, 1} // regular
+ }}};
+
+static NSControlStateValue CellStateForCheckboxOrRadioState(
+ nsNativeThemeCocoa::CheckboxOrRadioState aState) {
+ switch (aState) {
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eOff:
+ return NSOffState;
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eOn:
+ return NSOnState;
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eIndeterminate:
+ return NSMixedState;
+ }
+}
+
+void nsNativeThemeCocoa::DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox,
+ const HIRect& inBoxRect,
+ const CheckboxOrRadioParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSButtonCell* cell = inCheckbox ? mCheckboxCell : mRadioButtonCell;
+ ApplyControlParamsToNSCell(aParams.controlParams, cell);
+
+ [cell setState:CellStateForCheckboxOrRadioState(aParams.state)];
+ [cell setControlTint:(aParams.controlParams.insideActiveWindow ? [NSColor currentControlTint]
+ : NSClearControlTint)];
+
+ // Ensure that the control is square.
+ float length = std::min(inBoxRect.size.width, inBoxRect.size.height);
+ HIRect drawRect = CGRectMake(inBoxRect.origin.x + (int)((inBoxRect.size.width - length) / 2.0f),
+ inBoxRect.origin.y + (int)((inBoxRect.size.height - length) / 2.0f),
+ length, length);
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aParams.controlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(cell, cgContext, drawRect, inCheckbox ? checkboxSettings : radioSettings,
+ aParams.verticalAlignFactor, mCellDrawView, NO);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings searchFieldSettings = {{
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(32, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+static bool IsToolbarStyleContainer(nsIFrame* aFrame) {
+ nsIContent* content = aFrame->GetContent();
+ if (!content) {
+ return false;
+ }
+
+ if (content->IsAnyOfXULElements(nsGkAtoms::toolbar, nsGkAtoms::toolbox, nsGkAtoms::statusbar)) {
+ return true;
+ }
+
+ switch (aFrame->StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool IsInsideToolbar(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) {
+ if (IsToolbarStyleContainer(frame)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsNativeThemeCocoa::TextFieldParams nsNativeThemeCocoa::ComputeTextFieldParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ TextFieldParams params;
+ params.insideToolbar = IsInsideToolbar(aFrame);
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+
+ // See ShouldUnconditionallyDrawFocusRingIfFocused.
+ params.focused = aEventState.HasState(ElementState::FOCUS);
+
+ params.rtl = IsFrameRTL(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawTextField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSTextFieldCell* cell = mTextFieldCell;
+ [cell setEnabled:!aParams.disabled];
+ [cell setShowsFirstResponder:aParams.focused];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(cell, cgContext, inBoxRect, searchFieldSettings, aParams.verticalAlignFactor,
+ mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mSearchFieldCell.enabled = !aParams.disabled;
+ mSearchFieldCell.showsFirstResponder = aParams.focused;
+ mSearchFieldCell.placeholderString = @"";
+ mSearchFieldCell.shouldUseToolbarStyle = aParams.insideToolbar;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(mSearchFieldCell, cgContext, inBoxRect, searchFieldSettings,
+ aParams.verticalAlignFactor, mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const NSSize kCheckmarkSize = NSMakeSize(11, 11);
+static const NSSize kMenuarrowSize = NSMakeSize(9, 10);
+static const NSSize kMenuScrollArrowSize = NSMakeSize(10, 8);
+static NSString* kCheckmarkImage = @"MenuOnState";
+static NSString* kMenuarrowRightImage = @"MenuSubmenu";
+static NSString* kMenuarrowLeftImage = @"MenuSubmenuLeft";
+static NSString* kMenuDownScrollArrowImage = @"MenuScrollDown";
+static NSString* kMenuUpScrollArrowImage = @"MenuScrollUp";
+static const CGFloat kMenuIconIndent = 6.0f;
+
+NSString* nsNativeThemeCocoa::GetMenuIconName(const MenuIconParams& aParams) {
+ switch (aParams.icon) {
+ case MenuIcon::eCheckmark:
+ return kCheckmarkImage;
+ case MenuIcon::eMenuArrow:
+ return aParams.rtl ? kMenuarrowLeftImage : kMenuarrowRightImage;
+ case MenuIcon::eMenuDownScrollArrow:
+ return kMenuDownScrollArrowImage;
+ case MenuIcon::eMenuUpScrollArrow:
+ return kMenuUpScrollArrowImage;
+ }
+}
+
+NSSize nsNativeThemeCocoa::GetMenuIconSize(MenuIcon aIcon) {
+ switch (aIcon) {
+ case MenuIcon::eCheckmark:
+ return kCheckmarkSize;
+ case MenuIcon::eMenuArrow:
+ return kMenuarrowSize;
+ case MenuIcon::eMenuDownScrollArrow:
+ case MenuIcon::eMenuUpScrollArrow:
+ return kMenuScrollArrowSize;
+ }
+}
+
+nsNativeThemeCocoa::MenuIconParams nsNativeThemeCocoa::ComputeMenuIconParams(
+ nsIFrame* aFrame, ElementState aEventState, MenuIcon aIcon) {
+ bool isDisabled = aEventState.HasState(ElementState::DISABLED);
+
+ MenuIconParams params;
+ params.icon = aIcon;
+ params.disabled = isDisabled;
+ params.insideActiveMenuItem = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive);
+ params.centerHorizontally = true;
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect,
+ const MenuIconParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSSize size = GetMenuIconSize(aParams.icon);
+
+ // Adjust size and position of our drawRect.
+ CGFloat paddingX = std::max(CGFloat(0.0), aRect.size.width - size.width);
+ CGFloat paddingY = std::max(CGFloat(0.0), aRect.size.height - size.height);
+ CGFloat paddingStartX = std::min(paddingX, kMenuIconIndent);
+ CGFloat paddingEndX = std::max(CGFloat(0.0), paddingX - kMenuIconIndent);
+ CGRect drawRect = CGRectMake(aRect.origin.x + (aParams.centerHorizontally ? ceil(paddingX / 2)
+ : aParams.rtl ? paddingEndX
+ : paddingStartX),
+ aRect.origin.y + ceil(paddingY / 2), size.width, size.height);
+
+ NSString* state;
+ if (aParams.disabled) {
+ state = @"disabled";
+ } else if (aParams.insideActiveMenuItem) {
+ state = @"pressed";
+ } else if (IsDarkAppearance(NSAppearance.currentAppearance)) {
+ // CUIDraw draws the image with a color that's too faint for the dark
+ // appearance. The "pressed" state happens to use white, which looks better
+ // and matches the white text color, so use it instead of "normal".
+ state = @"pressed";
+ } else {
+ state = @"normal";
+ }
+
+ NSString* imageName = GetMenuIconName(aParams);
+
+ RenderWithCoreUI(
+ drawRect, cgContext,
+ [NSDictionary dictionaryWithObjectsAndKeys:@"kCUIBackgroundTypeMenu", @"backgroundTypeKey",
+ imageName, @"imageNameKey", state, @"state",
+ @"image", @"widget", [NSNumber numberWithBool:YES],
+ @"is.flipped", nil]);
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, drawRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsNativeThemeCocoa::MenuItemParams nsNativeThemeCocoa::ComputeMenuItemParams(
+ nsIFrame* aFrame, ElementState aEventState, bool aIsChecked) {
+ bool isDisabled = aEventState.HasState(ElementState::DISABLED);
+
+ MenuItemParams params;
+ params.checked = aIsChecked;
+ params.disabled = isDisabled;
+ params.selected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive);
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawMenuItem(CGContextRef cgContext, const CGRect& inBoxRect,
+ const MenuItemParams& aParams) {
+ if (aParams.checked) {
+ MenuIconParams params;
+ params.disabled = aParams.disabled;
+ params.insideActiveMenuItem = aParams.selected;
+ params.rtl = aParams.rtl;
+ params.icon = MenuIcon::eCheckmark;
+ DrawMenuIcon(cgContext, inBoxRect, params);
+ }
+}
+
+void nsNativeThemeCocoa::DrawMenuSeparator(CGContextRef cgContext, const CGRect& inBoxRect,
+ const MenuItemParams& aParams) {
+ // Workaround for visual artifacts issues with
+ // HIThemeDrawMenuSeparator on macOS Big Sur.
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+ CGRect separatorRect = inBoxRect;
+ separatorRect.size.height = 1;
+ separatorRect.size.width -= 42;
+ separatorRect.origin.x += 21;
+ if (!IsDarkAppearance(NSAppearance.currentAppearance)) {
+ // Use transparent black with an alpha similar to the native separator.
+ // The values 231 (menu background) and 205 (separator color) have been
+ // sampled from a window screenshot of a native context menu.
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.0, (231 - 205) / 231.0);
+ } else {
+ // Similar to above, use white with an alpha. The values 45 (menu
+ // background) and 81 (separator color) were sampled on macOS 12 with the
+ // "Reduce transparency" system setting turned on.
+ CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0 + ((45 - 81) / 45.0));
+ }
+ CGContextFillRect(cgContext, separatorRect);
+ return;
+ }
+
+ ThemeMenuState menuState;
+ if (aParams.disabled) {
+ menuState = kThemeMenuDisabled;
+ } else {
+ menuState = aParams.selected ? kThemeMenuSelected : kThemeMenuActive;
+ }
+
+ HIThemeMenuItemDrawInfo midi = {0, kThemeMenuItemPlain, menuState};
+ HIThemeDrawMenuSeparator(&inBoxRect, &inBoxRect, &midi, cgContext, HITHEME_ORIENTATION);
+}
+
+static bool ShouldUnconditionallyDrawFocusRingIfFocused(nsIFrame* aFrame) {
+ // Mac always draws focus rings for textboxes and lists.
+ switch (aFrame->StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Listbox:
+ return true;
+ default:
+ return false;
+ }
+}
+
+nsNativeThemeCocoa::ControlParams nsNativeThemeCocoa::ComputeControlParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ ControlParams params;
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.pressed = aEventState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER);
+ params.focused = aEventState.HasState(ElementState::FOCUS) &&
+ (aEventState.HasState(ElementState::FOCUSRING) ||
+ ShouldUnconditionallyDrawFocusRingIfFocused(aFrame));
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+static const NSSize kHelpButtonSize = NSMakeSize(20, 20);
+static const NSSize kDisclosureButtonSize = NSMakeSize(21, 21);
+
+static const CellRenderSettings pushButtonSettings = {{
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(26, 0), // small
+ NSMakeSize(30, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {4, 0, 4, 1}, // small
+ {5, 0, 5, 2} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {4, 0, 4, 1}, // small
+ {5, 0, 5, 2} // regular
+ }}};
+
+// The height at which we start doing square buttons instead of rounded buttons
+// Rounded buttons look bad if drawn at a height greater than 26, so at that point
+// we switch over to doing square buttons which looks fine at any size.
+#define DO_SQUARE_BUTTON_HEIGHT 26
+
+void nsNativeThemeCocoa::DrawPushButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ButtonType aButtonType, ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mPushButtonCell);
+ [mPushButtonCell setBezelStyle:NSRoundedBezelStyle];
+ mPushButtonCell.keyEquivalent = aButtonType == ButtonType::eDefaultPushButton ? @"\r" : @"";
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(mPushButtonCell, cgContext, inBoxRect, pushButtonSettings, 0.5f,
+ mCellDrawView, aControlParams.rtl, 1.0f);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSquareBezelPushButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mPushButtonCell);
+ [mPushButtonCell setBezelStyle:NSShadowlessSquareBezelStyle];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mPushButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize,
+ NSMakeSize(14, 0), NULL, mCellDrawView, aControlParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawHelpButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mHelpButtonCell);
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mHelpButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize,
+ kHelpButtonSize, NULL, mCellDrawView,
+ false); // Don't mirror icon in RTL.
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawDisclosureButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams,
+ NSControlStateValue aCellState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mDisclosureButtonCell);
+ [mDisclosureButtonCell setState:aCellState];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mDisclosureButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize,
+ kDisclosureButtonSize, NULL, mCellDrawView,
+ false); // Don't mirror icon in RTL.
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+typedef void (*RenderHIThemeControlFunction)(CGContextRef cgContext, const HIRect& aRenderRect,
+ void* aData);
+
+static void RenderTransformedHIThemeControl(CGContextRef aCGContext, const HIRect& aRect,
+ RenderHIThemeControlFunction aFunc, void* aData,
+ BOOL mirrorHorizontally = NO) {
+ CGAffineTransform savedCTM = CGContextGetCTM(aCGContext);
+ CGContextTranslateCTM(aCGContext, aRect.origin.x, aRect.origin.y);
+
+ bool drawDirect;
+ HIRect drawRect = aRect;
+ drawRect.origin = CGPointZero;
+
+ if (!mirrorHorizontally && savedCTM.a == 1.0f && savedCTM.b == 0.0f && savedCTM.c == 0.0f &&
+ (savedCTM.d == 1.0f || savedCTM.d == -1.0f)) {
+ drawDirect = TRUE;
+ } else {
+ drawDirect = FALSE;
+ }
+
+ // Fall back to no bitmap buffer if the area of our control (in pixels^2)
+ // is too large.
+ if (drawDirect || (aRect.size.width * aRect.size.height > BITMAP_MAX_AREA)) {
+ aFunc(aCGContext, drawRect, aData);
+ } else {
+ // Inflate the buffer to capture focus rings.
+ int w = ceil(drawRect.size.width) + 2 * kMaxFocusRingWidth;
+ int h = ceil(drawRect.size.height) + 2 * kMaxFocusRingWidth;
+
+ int backingScaleFactor = GetBackingScaleFactorForRendering(aCGContext);
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+ CGContextRef bitmapctx = CGBitmapContextCreate(
+ NULL, w * backingScaleFactor, h * backingScaleFactor, 8, w * backingScaleFactor * 4,
+ colorSpace, kCGImageAlphaPremultipliedFirst);
+ CGColorSpaceRelease(colorSpace);
+
+ CGContextScaleCTM(bitmapctx, backingScaleFactor, backingScaleFactor);
+ CGContextTranslateCTM(bitmapctx, kMaxFocusRingWidth, kMaxFocusRingWidth);
+
+ // Set the context's "base transform" to in order to get correctly-sized focus rings.
+ CGContextSetBaseCTM(bitmapctx,
+ CGAffineTransformMakeScale(backingScaleFactor, backingScaleFactor));
+
+ // HITheme always wants to draw into a flipped context, or things
+ // get confused.
+ CGContextTranslateCTM(bitmapctx, 0.0f, aRect.size.height);
+ CGContextScaleCTM(bitmapctx, 1.0f, -1.0f);
+
+ aFunc(bitmapctx, drawRect, aData);
+
+ CGImageRef bitmap = CGBitmapContextCreateImage(bitmapctx);
+
+ CGAffineTransform ctm = CGContextGetCTM(aCGContext);
+
+ // We need to unflip, so that we can do a DrawImage without getting a flipped image.
+ CGContextTranslateCTM(aCGContext, 0.0f, aRect.size.height);
+ CGContextScaleCTM(aCGContext, 1.0f, -1.0f);
+
+ if (mirrorHorizontally) {
+ CGContextTranslateCTM(aCGContext, aRect.size.width, 0);
+ CGContextScaleCTM(aCGContext, -1.0f, 1.0f);
+ }
+
+ HIRect inflatedDrawRect = CGRectMake(-kMaxFocusRingWidth, -kMaxFocusRingWidth, w, h);
+ CGContextDrawImage(aCGContext, inflatedDrawRect, bitmap);
+
+ CGContextSetCTM(aCGContext, ctm);
+
+ CGImageRelease(bitmap);
+ CGContextRelease(bitmapctx);
+ }
+
+ CGContextSetCTM(aCGContext, savedCTM);
+}
+
+static void RenderButton(CGContextRef cgContext, const HIRect& aRenderRect, void* aData) {
+ HIThemeButtonDrawInfo* bdi = (HIThemeButtonDrawInfo*)aData;
+ HIThemeDrawButton(&aRenderRect, bdi, cgContext, kHIThemeOrientationNormal, NULL);
+}
+
+static ThemeDrawState ToThemeDrawState(const nsNativeThemeCocoa::ControlParams& aParams) {
+ if (aParams.disabled) {
+ return kThemeStateUnavailable;
+ }
+ if (aParams.pressed) {
+ return kThemeStatePressed;
+ }
+ return kThemeStateActive;
+}
+
+void nsNativeThemeCocoa::DrawHIThemeButton(CGContextRef cgContext, const HIRect& aRect,
+ ThemeButtonKind aKind, ThemeButtonValue aValue,
+ ThemeDrawState aState, ThemeButtonAdornment aAdornment,
+ const ControlParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi;
+ bdi.version = 0;
+ bdi.kind = aKind;
+ bdi.value = aValue;
+ bdi.state = aState;
+ bdi.adornment = aAdornment;
+
+ if (aParams.focused && aParams.insideActiveWindow) {
+ bdi.adornment |= kThemeAdornmentFocus;
+ }
+
+ RenderTransformedHIThemeControl(cgContext, aRect, RenderButton, &bdi, aParams.rtl);
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, inBoxRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ const ButtonParams& aParams) {
+ ControlParams controlParams = aParams.controlParams;
+
+ switch (aParams.button) {
+ case ButtonType::eRegularPushButton:
+ case ButtonType::eDefaultPushButton:
+ DrawPushButton(cgContext, inBoxRect, aParams.button, controlParams);
+ return;
+ case ButtonType::eSquareBezelPushButton:
+ DrawSquareBezelPushButton(cgContext, inBoxRect, controlParams);
+ return;
+ case ButtonType::eArrowButton:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeArrowButton, kThemeButtonOn,
+ kThemeStateUnavailable, kThemeAdornmentArrowDownArrow, controlParams);
+ return;
+ case ButtonType::eHelpButton:
+ DrawHelpButton(cgContext, inBoxRect, controlParams);
+ return;
+ case ButtonType::eTreeTwistyPointingRight:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton, kThemeDisclosureRight,
+ ToThemeDrawState(controlParams), kThemeAdornmentNone, controlParams);
+ return;
+ case ButtonType::eTreeTwistyPointingDown:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton, kThemeDisclosureDown,
+ ToThemeDrawState(controlParams), kThemeAdornmentNone, controlParams);
+ return;
+ case ButtonType::eDisclosureButtonClosed:
+ DrawDisclosureButton(cgContext, inBoxRect, controlParams, NSOffState);
+ return;
+ case ButtonType::eDisclosureButtonOpen:
+ DrawDisclosureButton(cgContext, inBoxRect, controlParams, NSOnState);
+ return;
+ }
+}
+
+nsNativeThemeCocoa::TreeHeaderCellParams nsNativeThemeCocoa::ComputeTreeHeaderCellParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ TreeHeaderCellParams params;
+ params.controlParams = ComputeControlParams(aFrame, aEventState);
+ params.sortDirection = GetTreeSortDirection(aFrame);
+ params.lastTreeHeaderCell = IsLastTreeHeaderCell(aFrame);
+ return params;
+}
+
+@interface NSTableHeaderCell (NSTableHeaderCell_setSortable)
+// This method has been present in the same form since at least macOS 10.4.
+- (void)_setSortable:(BOOL)arg1
+ showSortIndicator:(BOOL)arg2
+ ascending:(BOOL)arg3
+ priority:(NSInteger)arg4
+ highlightForSort:(BOOL)arg5;
+@end
+
+void nsNativeThemeCocoa::DrawTreeHeaderCell(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TreeHeaderCellParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Without clearing the cell's title, it takes on a default value of "Field",
+ // which is displayed underneath the title set in the front-end.
+ NSCell* cell = (NSCell*)mTreeHeaderCell;
+ cell.title = @"";
+
+ if ([mTreeHeaderCell respondsToSelector:@selector
+ (_setSortable:showSortIndicator:ascending:priority:highlightForSort:)]) {
+ switch (aParams.sortDirection) {
+ case eTreeSortDirection_Ascending:
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:YES
+ ascending:YES
+ priority:0
+ highlightForSort:YES];
+ break;
+ case eTreeSortDirection_Descending:
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:YES
+ ascending:NO
+ priority:0
+ highlightForSort:YES];
+ break;
+ default:
+ // eTreeSortDirection_Natural
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:NO
+ ascending:YES
+ priority:0
+ highlightForSort:NO];
+ break;
+ }
+ }
+
+ mTreeHeaderCell.enabled = !aParams.controlParams.disabled;
+ mTreeHeaderCell.state =
+ (mTreeHeaderCell.enabled && aParams.controlParams.pressed) ? NSOnState : NSOffState;
+
+ mCellDrawView._drawingEndSeparator = !aParams.lastTreeHeaderCell;
+
+ NSGraphicsContext* savedContext = NSGraphicsContext.currentContext;
+ NSGraphicsContext.currentContext = [NSGraphicsContext graphicsContextWithCGContext:cgContext
+ flipped:YES];
+ DrawCellIncludingFocusRing(mTreeHeaderCell, inBoxRect, mCellDrawView);
+ NSGraphicsContext.currentContext = savedContext;
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, inBoxRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings dropdownSettings = {{
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {1, 1, 2, 1}, // mini
+ {3, 0, 3, 1}, // small
+ {3, 0, 3, 0} // regular
+ },
+ {
+ // Yosemite
+ {1, 1, 2, 1}, // mini
+ {3, 0, 3, 1}, // small
+ {3, 0, 3, 0} // regular
+ }}};
+
+static const CellRenderSettings editableMenulistSettings = {{
+ NSMakeSize(0, 15), // mini
+ NSMakeSize(0, 18), // small
+ NSMakeSize(0, 21) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 2, 2}, // mini
+ {0, 0, 3, 2}, // small
+ {0, 1, 3, 3} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 2, 2}, // mini
+ {0, 0, 3, 2}, // small
+ {0, 1, 3, 3} // regular
+ }}};
+
+void nsNativeThemeCocoa::DrawDropdown(CGContextRef cgContext, const HIRect& inBoxRect,
+ const DropdownParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mDropdownCell setPullsDown:aParams.pullsDown];
+ NSCell* cell = aParams.editable ? (NSCell*)mComboBoxCell : (NSCell*)mDropdownCell;
+
+ ApplyControlParamsToNSCell(aParams.controlParams, cell);
+
+ if (aParams.controlParams.insideActiveWindow) {
+ [cell setControlTint:[NSColor currentControlTint]];
+ } else {
+ [cell setControlTint:NSClearControlTint];
+ }
+
+ const CellRenderSettings& settings =
+ aParams.editable ? editableMenulistSettings : dropdownSettings;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aParams.controlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(cell, cgContext, inBoxRect, settings, 0.5f, mCellDrawView,
+ aParams.controlParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings spinnerSettings = {
+ {
+ NSMakeSize(11, 16), // mini (width trimmed by 2px to reduce blank border)
+ NSMakeSize(15, 22), // small
+ NSMakeSize(19, 27) // regular
+ },
+ {
+ NSMakeSize(11, 16), // mini (width trimmed by 2px to reduce blank border)
+ NSMakeSize(15, 22), // small
+ NSMakeSize(19, 27) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+HIThemeButtonDrawInfo nsNativeThemeCocoa::SpinButtonDrawInfo(ThemeButtonKind aKind,
+ const SpinButtonParams& aParams) {
+ HIThemeButtonDrawInfo bdi;
+ bdi.version = 0;
+ bdi.kind = aKind;
+ bdi.value = kThemeButtonOff;
+ bdi.adornment = kThemeAdornmentNone;
+
+ if (aParams.disabled) {
+ bdi.state = kThemeStateUnavailable;
+ } else if (aParams.insideActiveWindow && aParams.pressedButton) {
+ if (*aParams.pressedButton == SpinButton::eUp) {
+ bdi.state = kThemeStatePressedUp;
+ } else {
+ bdi.state = kThemeStatePressedDown;
+ }
+ } else {
+ bdi.state = kThemeStateActive;
+ }
+
+ return bdi;
+}
+
+void nsNativeThemeCocoa::DrawSpinButtons(CGContextRef cgContext, const HIRect& inBoxRect,
+ const SpinButtonParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi = SpinButtonDrawInfo(kThemeIncDecButton, aParams);
+ HIThemeDrawButton(&inBoxRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSpinButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ SpinButton aDrawnButton, const SpinButtonParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi = SpinButtonDrawInfo(kThemeIncDecButtonMini, aParams);
+
+ // Cocoa only allows kThemeIncDecButton to paint the up and down spin buttons
+ // together as a single unit (presumably because when one button is active,
+ // the appearance of both changes (in different ways)). Here we have to paint
+ // both buttons, using clip to hide the one we don't want to paint.
+ HIRect drawRect = inBoxRect;
+ drawRect.size.height *= 2;
+ if (aDrawnButton == SpinButton::eDown) {
+ drawRect.origin.y -= inBoxRect.size.height;
+ }
+
+ // Shift the drawing a little to the left, since cocoa paints with more
+ // blank space around the visual buttons than we'd like:
+ drawRect.origin.x -= 1;
+
+ CGContextSaveGState(cgContext);
+ CGContextClipToRect(cgContext, inBoxRect);
+
+ HIThemeDrawButton(&drawRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings progressSettings[2][2] = {
+ // Vertical progress bar.
+ {// Determined settings.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(10, 0), // small
+ NSMakeSize(16, 0) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}},
+ // There is no horizontal margin in regular undetermined size.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(10, 0), // small
+ NSMakeSize(16, 0) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 0, 1, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 0, 1, 0} // regular
+ }}}},
+ // Horizontal progress bar.
+ {// Determined settings.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(0, 10), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}},
+ // There is no horizontal margin in regular undetermined size.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(0, 10), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {0, 1, 0, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {0, 1, 0, 1} // regular
+ }}}}};
+
+nsNativeThemeCocoa::ProgressParams nsNativeThemeCocoa::ComputeProgressParams(
+ nsIFrame* aFrame, ElementState aEventState, bool aIsHorizontal) {
+ ProgressParams params;
+ params.value = GetProgressValue(aFrame);
+ params.max = GetProgressMaxValue(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.indeterminate = aEventState.HasState(ElementState::INDETERMINATE);
+ params.horizontal = aIsHorizontal;
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawProgress(CGContextRef cgContext, const HIRect& inBoxRect,
+ const ProgressParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSProgressBarCell* cell = mProgressBarCell;
+
+ [cell setValue:aParams.value];
+ [cell setMax:aParams.max];
+ [cell setIndeterminate:aParams.indeterminate];
+ [cell setHorizontal:aParams.horizontal];
+ [cell setControlTint:(aParams.insideActiveWindow ? [NSColor currentControlTint]
+ : NSClearControlTint)];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(cell, cgContext, inBoxRect,
+ progressSettings[aParams.horizontal][aParams.indeterminate],
+ aParams.verticalAlignFactor, mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings meterSetting = {{
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 16), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {1, 1, 1, 1}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ },
+ {
+ // Yosemite
+ {1, 1, 1, 1}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}};
+
+nsNativeThemeCocoa::MeterParams nsNativeThemeCocoa::ComputeMeterParams(nsIFrame* aFrame) {
+ nsIContent* content = aFrame->GetContent();
+ if (!(content && content->IsHTMLElement(nsGkAtoms::meter))) {
+ return MeterParams();
+ }
+
+ HTMLMeterElement* meterElement = static_cast<HTMLMeterElement*>(content);
+ MeterParams params;
+ params.value = meterElement->Value();
+ params.min = meterElement->Min();
+ params.max = meterElement->Max();
+ ElementState states = meterElement->State();
+ if (states.HasState(ElementState::SUB_OPTIMUM)) {
+ params.optimumState = OptimumState::eSubOptimum;
+ } else if (states.HasState(ElementState::SUB_SUB_OPTIMUM)) {
+ params.optimumState = OptimumState::eSubSubOptimum;
+ }
+ params.horizontal = !IsVerticalMeter(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ params.rtl = IsFrameRTL(aFrame);
+
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawMeter(CGContextRef cgContext, const HIRect& inBoxRect,
+ const MeterParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ NSLevelIndicatorCell* cell = mMeterBarCell;
+
+ [cell setMinValue:aParams.min];
+ [cell setMaxValue:aParams.max];
+ [cell setDoubleValue:aParams.value];
+
+ /**
+ * The way HTML and Cocoa defines the meter/indicator widget are different.
+ * So, we are going to use a trick to get the Cocoa widget showing what we
+ * are expecting: we set the warningValue or criticalValue to the current
+ * value when we want to have the widget to be in the warning or critical
+ * state.
+ */
+ switch (aParams.optimumState) {
+ case OptimumState::eOptimum:
+ [cell setWarningValue:aParams.max + 1];
+ [cell setCriticalValue:aParams.max + 1];
+ break;
+ case OptimumState::eSubOptimum:
+ [cell setWarningValue:aParams.value];
+ [cell setCriticalValue:aParams.max + 1];
+ break;
+ case OptimumState::eSubSubOptimum:
+ [cell setWarningValue:aParams.max + 1];
+ [cell setCriticalValue:aParams.value];
+ break;
+ }
+
+ HIRect rect = CGRectStandardize(inBoxRect);
+ BOOL vertical = !aParams.horizontal;
+
+ CGContextSaveGState(cgContext);
+
+ if (vertical) {
+ /**
+ * Cocoa doesn't provide a vertical meter bar so to show one, we have to
+ * show a rotated horizontal meter bar.
+ * Given that we want to show a vertical meter bar, we assume that the rect
+ * has vertical dimensions but we can't correctly draw a meter widget inside
+ * such a rectangle so we need to inverse width and height (and re-position)
+ * to get a rectangle with horizontal dimensions.
+ * Finally, we want to show a vertical meter so we want to rotate the result
+ * so it is vertical. We do that by changing the context.
+ */
+ CGFloat tmp = rect.size.width;
+ rect.size.width = rect.size.height;
+ rect.size.height = tmp;
+ rect.origin.x += rect.size.height / 2.f - rect.size.width / 2.f;
+ rect.origin.y += rect.size.width / 2.f - rect.size.height / 2.f;
+
+ CGContextTranslateCTM(cgContext, CGRectGetMidX(rect), CGRectGetMidY(rect));
+ CGContextRotateCTM(cgContext, -M_PI / 2.f);
+ CGContextTranslateCTM(cgContext, -CGRectGetMidX(rect), -CGRectGetMidY(rect));
+ }
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(cell, cgContext, rect, meterSetting, aParams.verticalAlignFactor,
+ mCellDrawView, !vertical && aParams.rtl);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+void nsNativeThemeCocoa::DrawTabPanel(CGContextRef cgContext, const HIRect& inBoxRect,
+ bool aIsInsideActiveWindow) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeTabPaneDrawInfo tpdi;
+
+ tpdi.version = 1;
+ tpdi.state = aIsInsideActiveWindow ? kThemeStateActive : kThemeStateInactive;
+ tpdi.direction = kThemeTabNorth;
+ tpdi.size = kHIThemeTabSizeNormal;
+ tpdi.kind = kHIThemeTabKindNormal;
+
+ HIThemeDrawTabPane(&inBoxRect, &tpdi, cgContext, HITHEME_ORIENTATION);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+Maybe<nsNativeThemeCocoa::ScaleParams> nsNativeThemeCocoa::ComputeHTMLScaleParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(aFrame);
+ if (!rangeFrame) {
+ return Nothing();
+ }
+
+ bool isHorizontal = IsRangeHorizontal(aFrame);
+
+ // ScaleParams requires integer min, max and value. This is purely for
+ // drawing, so we normalize to a range 0-1000 here.
+ ScaleParams params;
+ params.value = int32_t(rangeFrame->GetValueAsFractionOfRange() * 1000);
+ params.min = 0;
+ params.max = 1000;
+ params.reverse = !isHorizontal || rangeFrame->IsRightToLeft();
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.focused = aEventState.HasState(ElementState::FOCUSRING);
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+ params.horizontal = isHorizontal;
+ return Some(params);
+}
+
+void nsNativeThemeCocoa::DrawScale(CGContextRef cgContext, const HIRect& inBoxRect,
+ const ScaleParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeTrackDrawInfo tdi;
+
+ tdi.version = 0;
+ tdi.kind = kThemeMediumSlider;
+ tdi.bounds = inBoxRect;
+ tdi.min = aParams.min;
+ tdi.max = aParams.max;
+ tdi.value = aParams.value;
+ tdi.attributes = kThemeTrackShowThumb;
+ if (aParams.horizontal) {
+ tdi.attributes |= kThemeTrackHorizontal;
+ }
+ if (aParams.reverse) {
+ tdi.attributes |= kThemeTrackRightToLeft;
+ }
+ if (aParams.focused) {
+ tdi.attributes |= kThemeTrackHasFocus;
+ }
+ if (aParams.disabled) {
+ tdi.enableState = kThemeTrackDisabled;
+ } else {
+ tdi.enableState = aParams.insideActiveWindow ? kThemeTrackActive : kThemeTrackInactive;
+ }
+ tdi.trackInfo.slider.thumbDir = kThemeThumbPlain;
+ tdi.trackInfo.slider.pressState = 0;
+
+ HIThemeDrawTrack(&tdi, NULL, cgContext, HITHEME_ORIENTATION);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsIFrame* nsNativeThemeCocoa::SeparatorResponsibility(nsIFrame* aBefore, nsIFrame* aAfter) {
+ // Usually a separator is drawn by the segment to the right of the
+ // separator, but pressed and selected segments have higher priority.
+ if (!aBefore || !aAfter) return nullptr;
+ if (IsSelectedButton(aAfter)) return aAfter;
+ if (IsSelectedButton(aBefore) || IsPressedButton(aBefore)) return aBefore;
+ return aAfter;
+}
+
+static CGRect SeparatorAdjustedRect(CGRect aRect, nsNativeThemeCocoa::SegmentParams aParams) {
+ // A separator between two segments should always be located in the leftmost
+ // pixel column of the segment to the right of the separator, regardless of
+ // who ends up drawing it.
+ // CoreUI draws the separators inside the drawing rect.
+ if (!aParams.atLeftEnd && !aParams.drawsLeftSeparator) {
+ // The segment to the left of us draws the separator, so we need to make
+ // room for it.
+ aRect.origin.x += 1;
+ aRect.size.width -= 1;
+ }
+ if (aParams.drawsRightSeparator) {
+ // We draw the right separator, so we need to extend the draw rect into the
+ // segment to our right.
+ aRect.size.width += 1;
+ }
+ return aRect;
+}
+
+static NSString* ToolbarButtonPosition(BOOL aIsFirst, BOOL aIsLast) {
+ if (aIsFirst) {
+ if (aIsLast) return @"kCUISegmentPositionOnly";
+ return @"kCUISegmentPositionFirst";
+ }
+ if (aIsLast) return @"kCUISegmentPositionLast";
+ return @"kCUISegmentPositionMiddle";
+}
+
+struct SegmentedControlRenderSettings {
+ const CGFloat* heights;
+ const NSString* widgetName;
+};
+
+static const CGFloat tabHeights[3] = {17, 20, 23};
+
+static const SegmentedControlRenderSettings tabRenderSettings = {tabHeights, @"tab"};
+
+static const CGFloat toolbarButtonHeights[3] = {15, 18, 22};
+
+static const SegmentedControlRenderSettings toolbarButtonRenderSettings = {
+ toolbarButtonHeights, @"kCUIWidgetButtonSegmentedSCurve"};
+
+nsNativeThemeCocoa::SegmentParams nsNativeThemeCocoa::ComputeSegmentParams(
+ nsIFrame* aFrame, ElementState aEventState, SegmentType aSegmentType) {
+ SegmentParams params;
+ params.segmentType = aSegmentType;
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.pressed = IsPressedButton(aFrame);
+ params.selected = IsSelectedButton(aFrame);
+ params.focused = aEventState.HasState(ElementState::FOCUSRING);
+ bool isRTL = IsFrameRTL(aFrame);
+ nsIFrame* left = GetAdjacentSiblingFrameWithSameAppearance(aFrame, isRTL);
+ nsIFrame* right = GetAdjacentSiblingFrameWithSameAppearance(aFrame, !isRTL);
+ params.atLeftEnd = !left;
+ params.atRightEnd = !right;
+ params.drawsLeftSeparator = SeparatorResponsibility(left, aFrame) == aFrame;
+ params.drawsRightSeparator = SeparatorResponsibility(aFrame, right) == aFrame;
+ params.rtl = isRTL;
+ return params;
+}
+
+static SegmentedControlRenderSettings RenderSettingsForSegmentType(
+ nsNativeThemeCocoa::SegmentType aSegmentType) {
+ switch (aSegmentType) {
+ case nsNativeThemeCocoa::SegmentType::eToolbarButton:
+ return toolbarButtonRenderSettings;
+ case nsNativeThemeCocoa::SegmentType::eTab:
+ return tabRenderSettings;
+ }
+}
+
+void nsNativeThemeCocoa::DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect,
+ const SegmentParams& aParams) {
+ SegmentedControlRenderSettings renderSettings = RenderSettingsForSegmentType(aParams.segmentType);
+ NSControlSize controlSize = FindControlSize(inBoxRect.size.height, renderSettings.heights, 4.0f);
+ CGRect drawRect = SeparatorAdjustedRect(inBoxRect, aParams);
+
+ NSDictionary* dict = @{
+ @"widget" : renderSettings.widgetName,
+ @"kCUIPresentationStateKey" : (aParams.insideActiveWindow ? @"kCUIPresentationStateActiveKey"
+ : @"kCUIPresentationStateInactive"),
+ @"kCUIPositionKey" : ToolbarButtonPosition(aParams.atLeftEnd, aParams.atRightEnd),
+ @"kCUISegmentLeadingSeparatorKey" : [NSNumber numberWithBool:aParams.drawsLeftSeparator],
+ @"kCUISegmentTrailingSeparatorKey" : [NSNumber numberWithBool:aParams.drawsRightSeparator],
+ @"value" : [NSNumber numberWithBool:aParams.selected],
+ @"state" :
+ (aParams.pressed ? @"pressed" : (aParams.insideActiveWindow ? @"normal" : @"inactive")),
+ @"focus" : [NSNumber numberWithBool:aParams.focused],
+ @"size" : CUIControlSizeForCocoaSize(controlSize),
+ @"is.flipped" : [NSNumber numberWithBool:YES],
+ @"direction" : @"up"
+ };
+
+ RenderWithCoreUI(drawRect, cgContext, dict);
+}
+
+void nsNativeThemeCocoa::DrawToolbar(CGContextRef cgContext, const CGRect& inBoxRect,
+ bool aIsMain) {
+ CGRect drawRect = inBoxRect;
+
+ // top border
+ drawRect.size.height = 1.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarTopBorderGrey, drawRect, aIsMain);
+
+ // background
+ drawRect.origin.y += drawRect.size.height;
+ drawRect.size.height = inBoxRect.size.height - 2.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarFillGrey, drawRect, aIsMain);
+
+ // bottom border
+ drawRect.origin.y += drawRect.size.height;
+ drawRect.size.height = 1.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarBottomBorderGrey, drawRect, aIsMain);
+}
+
+static bool ToolbarCanBeUnified(const gfx::Rect& aRect, NSWindow* aWindow) {
+ if (![aWindow isKindOfClass:[ToolbarWindow class]]) return false;
+
+ ToolbarWindow* win = (ToolbarWindow*)aWindow;
+ float unifiedToolbarHeight = [win unifiedToolbarHeight];
+ return aRect.X() == 0 && aRect.Width() >= [win frame].size.width &&
+ aRect.YMost() <= unifiedToolbarHeight;
+}
+
+void nsNativeThemeCocoa::DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect,
+ bool aIsMain) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (inBoxRect.size.height < 2.0f) return;
+
+ CGContextSaveGState(cgContext);
+ CGContextClipToRect(cgContext, inBoxRect);
+
+ // kCUIWidgetWindowFrame draws a complete window frame with both title bar
+ // and bottom bar. We only want the bottom bar, so we extend the draw rect
+ // upwards to make space for the title bar, and then we clip it away.
+ CGRect drawRect = inBoxRect;
+ const int extendUpwards = 40;
+ drawRect.origin.y -= extendUpwards;
+ drawRect.size.height += extendUpwards;
+ RenderWithCoreUI(
+ drawRect, cgContext,
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:@"kCUIWidgetWindowFrame", @"widget", @"regularwin",
+ @"windowtype", (aIsMain ? @"normal" : @"inactive"), @"state",
+ [NSNumber numberWithInt:inBoxRect.size.height],
+ @"kCUIWindowFrameBottomBarHeightKey",
+ [NSNumber numberWithBool:YES],
+ @"kCUIWindowFrameDrawBottomBarSeparatorKey",
+ [NSNumber numberWithBool:YES], @"is.flipped", nil]);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawMultilineTextField(CGContextRef cgContext, const CGRect& inBoxRect,
+ bool aIsFocused) {
+ mTextFieldCell.enabled = YES;
+ mTextFieldCell.showsFirstResponder = aIsFocused;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = YES;
+ }
+
+ // DrawCellIncludingFocusRing draws into the current NSGraphicsContext, so do the usual
+ // save+restore dance.
+ NSGraphicsContext* savedContext = NSGraphicsContext.currentContext;
+ NSGraphicsContext.currentContext = [NSGraphicsContext graphicsContextWithCGContext:cgContext
+ flipped:YES];
+ DrawCellIncludingFocusRing(mTextFieldCell, inBoxRect, mCellDrawView);
+ NSGraphicsContext.currentContext = savedContext;
+}
+
+void nsNativeThemeCocoa::DrawSourceListSelection(CGContextRef aContext, const CGRect& aRect,
+ bool aWindowIsActive, bool aSelectionIsActive) {
+ NSColor* fillColor;
+ if (aSelectionIsActive) {
+ // Active selection, blue or graphite.
+ fillColor = ControlAccentColor();
+ } else {
+ // Inactive selection, gray.
+ if (aWindowIsActive) {
+ fillColor = [NSColor colorWithWhite:0.871 alpha:1.0];
+ } else {
+ fillColor = [NSColor colorWithWhite:0.808 alpha:1.0];
+ }
+ }
+ CGContextSetFillColorWithColor(aContext, [fillColor CGColor]);
+ CGContextFillRect(aContext, aRect);
+}
+
+static bool IsHiDPIContext(nsDeviceContext* aContext) {
+ return AppUnitsPerCSSPixel() >= 2 * aContext->AppUnitsPerDevPixelAtUnitFullZoom();
+}
+
+Maybe<nsNativeThemeCocoa::WidgetInfo> nsNativeThemeCocoa::ComputeWidgetInfo(
+ nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // setup to draw into the correct port
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ gfx::Rect nativeWidgetRect(aRect.x, aRect.y, aRect.width, aRect.height);
+ nativeWidgetRect.Scale(1.0 / gfxFloat(p2a));
+ float originalHeight = nativeWidgetRect.Height();
+ nativeWidgetRect.Round();
+ if (nativeWidgetRect.IsEmpty()) {
+ return Nothing(); // Don't attempt to draw invisible widgets.
+ }
+
+ bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext());
+ if (hidpi) {
+ // Use high-resolution drawing.
+ nativeWidgetRect.Scale(0.5f);
+ originalHeight *= 0.5f;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ switch (aAppearance) {
+ case StyleAppearance::Menupopup:
+ return Nothing();
+
+ case StyleAppearance::Menuarrow:
+ return Some(
+ WidgetInfo::MenuIcon(ComputeMenuIconParams(aFrame, elementState, MenuIcon::eMenuArrow)));
+
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ return Some(WidgetInfo::MenuItem(ComputeMenuItemParams(
+ aFrame, elementState, aAppearance == StyleAppearance::Checkmenuitem)));
+
+ case StyleAppearance::Menuseparator:
+ return Some(WidgetInfo::MenuSeparator(ComputeMenuItemParams(aFrame, elementState, false)));
+
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown: {
+ MenuIcon icon = aAppearance == StyleAppearance::ButtonArrowUp
+ ? MenuIcon::eMenuUpScrollArrow
+ : MenuIcon::eMenuDownScrollArrow;
+ return Some(WidgetInfo::MenuIcon(ComputeMenuIconParams(aFrame, elementState, icon)));
+ }
+
+ case StyleAppearance::Tooltip:
+ return Nothing();
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ bool isCheckbox = (aAppearance == StyleAppearance::Checkbox);
+
+ CheckboxOrRadioParams params;
+ params.state = CheckboxOrRadioState::eOff;
+ if (elementState.HasState(ElementState::INDETERMINATE)) {
+ params.state = CheckboxOrRadioState::eIndeterminate;
+ } else if (elementState.HasState(ElementState::CHECKED)) {
+ params.state = CheckboxOrRadioState::eOn;
+ }
+ params.controlParams = ComputeControlParams(aFrame, elementState);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ if (isCheckbox) {
+ return Some(WidgetInfo::Checkbox(params));
+ }
+ return Some(WidgetInfo::Radio(params));
+ }
+
+ case StyleAppearance::Button:
+ if (IsDefaultButton(aFrame)) {
+ // Check whether the default button is in a document that does not
+ // match the :-moz-window-inactive pseudoclass. This activeness check
+ // is different from the other "active window" checks in this file
+ // because we absolutely need the button's default button appearance to
+ // be in sync with its text color, and the text color is changed by
+ // such a :-moz-window-inactive rule. (That's because on 10.10 and up,
+ // default buttons in active windows have blue background and white
+ // text, and default buttons in inactive windows have white background
+ // and black text.)
+ DocumentState docState = aFrame->GetContent()->OwnerDoc()->GetDocumentState();
+ ControlParams params = ComputeControlParams(aFrame, elementState);
+ params.insideActiveWindow = !docState.HasState(DocumentState::WINDOW_INACTIVE);
+ return Some(WidgetInfo::Button(ButtonParams{params, ButtonType::eDefaultPushButton}));
+ }
+ if (IsButtonTypeMenu(aFrame)) {
+ ControlParams controlParams = ComputeControlParams(aFrame, elementState);
+ controlParams.pressed = IsOpenButton(aFrame);
+ DropdownParams params;
+ params.controlParams = controlParams;
+ params.pullsDown = true;
+ params.editable = false;
+ return Some(WidgetInfo::Dropdown(params));
+ }
+ if (originalHeight > DO_SQUARE_BUTTON_HEIGHT) {
+ // If the button is tall enough, draw the square button style so that
+ // buttons with non-standard content look good. Otherwise draw normal
+ // rounded aqua buttons.
+ // This comparison is done based on the height that is calculated without
+ // the top, because the snapped height can be affected by the top of the
+ // rect and that may result in different height depending on the top value.
+ return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eSquareBezelPushButton}));
+ }
+ return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eRegularPushButton}));
+
+ case StyleAppearance::MozMacHelpButton:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState), ButtonType::eHelpButton}));
+
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed: {
+ ButtonType buttonType = (aAppearance == StyleAppearance::MozMacDisclosureButtonClosed)
+ ? ButtonType::eDisclosureButtonClosed
+ : ButtonType::eDisclosureButtonOpen;
+ return Some(
+ WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), buttonType}));
+ }
+
+ case StyleAppearance::Spinner: {
+ bool isSpinner = (aAppearance == StyleAppearance::Spinner);
+ nsIContent* content = aFrame->GetContent();
+ if (isSpinner && content->IsHTMLElement()) {
+ // In HTML the theming for the spin buttons is drawn individually into
+ // their own backgrounds instead of being drawn into the background of
+ // their spinner parent as it is for XUL.
+ break;
+ }
+ SpinButtonParams params;
+ if (content->IsElement()) {
+ if (content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, u"up"_ns,
+ eCaseMatters)) {
+ params.pressedButton = Some(SpinButton::eUp);
+ } else if (content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state,
+ u"down"_ns, eCaseMatters)) {
+ params.pressedButton = Some(SpinButton::eDown);
+ }
+ }
+ params.disabled = elementState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+
+ return Some(WidgetInfo::SpinButtons(params));
+ }
+
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton: {
+ nsNumberControlFrame* numberControlFrame =
+ nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame);
+ if (numberControlFrame) {
+ SpinButtonParams params;
+ if (numberControlFrame->SpinnerUpButtonIsDepressed()) {
+ params.pressedButton = Some(SpinButton::eUp);
+ } else if (numberControlFrame->SpinnerDownButtonIsDepressed()) {
+ params.pressedButton = Some(SpinButton::eDown);
+ }
+ params.disabled = elementState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ if (aAppearance == StyleAppearance::SpinnerUpbutton) {
+ return Some(WidgetInfo::SpinButtonUp(params));
+ }
+ return Some(WidgetInfo::SpinButtonDown(params));
+ }
+ } break;
+
+ case StyleAppearance::Toolbarbutton: {
+ SegmentParams params =
+ ComputeSegmentParams(aFrame, elementState, SegmentType::eToolbarButton);
+ params.insideActiveWindow = [NativeWindowForFrame(aFrame) isMainWindow];
+ return Some(WidgetInfo::Segment(params));
+ }
+
+ case StyleAppearance::Separator:
+ return Some(WidgetInfo::Separator());
+
+ case StyleAppearance::Toolbar: {
+ NSWindow* win = NativeWindowForFrame(aFrame);
+ bool isMain = [win isMainWindow];
+ if (ToolbarCanBeUnified(nativeWidgetRect, win)) {
+ // Unified toolbars are drawn similar to vibrancy; we communicate their extents via the
+ // theme geometry mechanism and then place native views under Gecko's rendering. So Gecko
+ // just needs to be transparent in the place where the toolbar should be visible.
+ return Nothing();
+ }
+ return Some(WidgetInfo::Toolbar(isMain));
+ }
+
+ case StyleAppearance::MozWindowTitlebar: {
+ return Nothing();
+ }
+
+ case StyleAppearance::Statusbar:
+ return Some(WidgetInfo::StatusBar(IsActive(aFrame, YES)));
+
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist: {
+ ControlParams controlParams = ComputeControlParams(aFrame, elementState);
+ controlParams.pressed = IsOpenButton(aFrame);
+ DropdownParams params;
+ params.controlParams = controlParams;
+ params.pullsDown = false;
+ params.editable = false;
+ return Some(WidgetInfo::Dropdown(params));
+ }
+
+ case StyleAppearance::MozMenulistArrowButton:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState), ButtonType::eArrowButton}));
+
+ case StyleAppearance::Groupbox:
+ return Some(WidgetInfo::GroupBox());
+
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ return Some(WidgetInfo::TextField(ComputeTextFieldParams(aFrame, elementState)));
+
+ case StyleAppearance::Searchfield:
+ return Some(WidgetInfo::SearchField(ComputeTextFieldParams(aFrame, elementState)));
+
+ case StyleAppearance::ProgressBar: {
+ if (elementState.HasState(ElementState::INDETERMINATE)) {
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) {
+ NS_WARNING("Unable to animate progressbar!");
+ }
+ }
+ return Some(WidgetInfo::ProgressBar(
+ ComputeProgressParams(aFrame, elementState, !IsVerticalProgress(aFrame))));
+ }
+
+ case StyleAppearance::Meter:
+ return Some(WidgetInfo::Meter(ComputeMeterParams(aFrame)));
+
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Meterchunk:
+ // Do nothing: progress and meter bars cases will draw chunks.
+ break;
+
+ case StyleAppearance::Treetwisty:
+ return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eTreeTwistyPointingRight}));
+
+ case StyleAppearance::Treetwistyopen:
+ return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eTreeTwistyPointingDown}));
+
+ case StyleAppearance::Treeheadercell:
+ return Some(WidgetInfo::TreeHeaderCell(ComputeTreeHeaderCellParams(aFrame, elementState)));
+
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeview:
+ return Some(WidgetInfo::ColorFill(sRGBColor(1.0, 1.0, 1.0, 1.0)));
+
+ case StyleAppearance::Treeheader:
+ // do nothing, taken care of by individual header cells
+ case StyleAppearance::Treeheadersortarrow:
+ // do nothing, taken care of by treeview header
+ case StyleAppearance::Treeline:
+ // do nothing, these lines don't exist on macos
+ break;
+
+ case StyleAppearance::Range: {
+ Maybe<ScaleParams> params = ComputeHTMLScaleParams(aFrame, elementState);
+ if (params) {
+ return Some(WidgetInfo::Scale(*params));
+ }
+ break;
+ }
+
+ case StyleAppearance::Textarea:
+ return Some(WidgetInfo::MultilineTextField(elementState.HasState(ElementState::FOCUS)));
+
+ case StyleAppearance::Listbox:
+ return Some(WidgetInfo::ListBox());
+
+ case StyleAppearance::MozMacSourceList: {
+ return Nothing();
+ }
+
+ case StyleAppearance::MozMacSourceListSelection:
+ case StyleAppearance::MozMacActiveSourceListSelection: {
+ // We only support vibrancy for source list selections if we're inside
+ // a source list, because we need the background to be transparent.
+ if (IsInSourceList(aFrame)) {
+ return Nothing();
+ }
+ bool isInActiveWindow = FrameIsInActiveWindow(aFrame);
+ if (aAppearance == StyleAppearance::MozMacActiveSourceListSelection) {
+ return Some(WidgetInfo::ActiveSourceListSelection(isInActiveWindow));
+ }
+ return Some(WidgetInfo::InactiveSourceListSelection(isInActiveWindow));
+ }
+
+ case StyleAppearance::Tab: {
+ SegmentParams params = ComputeSegmentParams(aFrame, elementState, SegmentType::eTab);
+ params.pressed = params.pressed && !params.selected;
+ return Some(WidgetInfo::Segment(params));
+ }
+
+ case StyleAppearance::Tabpanels:
+ return Some(WidgetInfo::TabPanel(FrameIsInActiveWindow(aFrame)));
+
+ default:
+ break;
+ }
+
+ return Nothing();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(Nothing());
+}
+
+static bool IsWidgetNonNative(StyleAppearance aAppearance) {
+ return nsNativeTheme::IsWidgetScrollbarPart(aAppearance) ||
+ aAppearance == StyleAppearance::FocusOutline;
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect,
+ const nsRect& aDirtyRect, DrawOverflow aDrawOverflow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsWidgetNonNative(aAppearance)) {
+ return ThemeCocoa::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect, aDirtyRect,
+ aDrawOverflow);
+ }
+
+ Maybe<WidgetInfo> widgetInfo = ComputeWidgetInfo(aFrame, aAppearance, aRect);
+
+ if (!widgetInfo) {
+ return NS_OK;
+ }
+
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ gfx::Rect nativeWidgetRect = NSRectToRect(aRect, p2a);
+ nativeWidgetRect.Round();
+
+ bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext());
+
+ auto colorScheme = LookAndFeel::ColorSchemeForFrame(aFrame);
+
+ RenderWidget(*widgetInfo, colorScheme, *aContext->GetDrawTarget(), nativeWidgetRect,
+ NSRectToRect(aDirtyRect, p2a), hidpi ? 2.0f : 1.0f);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsNativeThemeCocoa::RenderWidget(const WidgetInfo& aWidgetInfo,
+ LookAndFeel::ColorScheme aScheme, DrawTarget& aDrawTarget,
+ const gfx::Rect& aWidgetRect, const gfx::Rect& aDirtyRect,
+ float aScale) {
+ // Some of the drawing below uses NSAppearance.currentAppearance behind the scenes.
+ // Set it to the appearance we want, the same way as nsLookAndFeel::NativeGetColor.
+ NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
+
+ // Also set the cell draw window's appearance; this is respected by NSTextFieldCell (and its
+ // subclass NSSearchFieldCell).
+ if (mCellDrawWindow) {
+ mCellDrawWindow.appearance = NSAppearance.currentAppearance;
+ }
+
+ const Widget widget = aWidgetInfo.Widget();
+
+ // Some widgets render using DrawTarget, and some using CGContext.
+ switch (widget) {
+ case Widget::eColorFill: {
+ sRGBColor color = aWidgetInfo.Params<sRGBColor>();
+ aDrawTarget.FillRect(aWidgetRect, ColorPattern(ToDeviceColor(color)));
+ break;
+ }
+ default: {
+ AutoRestoreTransform autoRestoreTransform(&aDrawTarget);
+ gfx::Rect widgetRect = aWidgetRect;
+ gfx::Rect dirtyRect = aDirtyRect;
+
+ dirtyRect.Scale(1.0f / aScale);
+ widgetRect.Scale(1.0f / aScale);
+ aDrawTarget.SetTransform(aDrawTarget.GetTransform().PreScale(aScale, aScale));
+
+ // The remaining widgets require a CGContext.
+ CGRect macRect =
+ CGRectMake(widgetRect.X(), widgetRect.Y(), widgetRect.Width(), widgetRect.Height());
+
+ gfxQuartzNativeDrawing nativeDrawing(aDrawTarget, dirtyRect);
+
+ CGContextRef cgContext = nativeDrawing.BeginNativeDrawing();
+ if (cgContext == nullptr) {
+ // The Quartz surface handles 0x0 surfaces by internally
+ // making all operations no-ops; there's no cgcontext created for them.
+ // Unfortunately, this means that callers that want to render
+ // directly to the CGContext need to be aware of this quirk.
+ return;
+ }
+
+ // Set the context's "base transform" to in order to get correctly-sized focus rings.
+ CGContextSetBaseCTM(cgContext, CGAffineTransformMakeScale(aScale, aScale));
+
+ switch (widget) {
+ case Widget::eColorFill:
+ MOZ_CRASH("already handled in outer switch");
+ break;
+ case Widget::eMenuIcon: {
+ MenuIconParams params = aWidgetInfo.Params<MenuIconParams>();
+ DrawMenuIcon(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMenuItem: {
+ MenuItemParams params = aWidgetInfo.Params<MenuItemParams>();
+ DrawMenuItem(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMenuSeparator: {
+ MenuItemParams params = aWidgetInfo.Params<MenuItemParams>();
+ DrawMenuSeparator(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eCheckbox: {
+ CheckboxOrRadioParams params = aWidgetInfo.Params<CheckboxOrRadioParams>();
+ DrawCheckboxOrRadio(cgContext, true, macRect, params);
+ break;
+ }
+ case Widget::eRadio: {
+ CheckboxOrRadioParams params = aWidgetInfo.Params<CheckboxOrRadioParams>();
+ DrawCheckboxOrRadio(cgContext, false, macRect, params);
+ break;
+ }
+ case Widget::eButton: {
+ ButtonParams params = aWidgetInfo.Params<ButtonParams>();
+ DrawButton(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eDropdown: {
+ DropdownParams params = aWidgetInfo.Params<DropdownParams>();
+ DrawDropdown(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSpinButtons: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButtons(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSpinButtonUp: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButton(cgContext, macRect, SpinButton::eUp, params);
+ break;
+ }
+ case Widget::eSpinButtonDown: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButton(cgContext, macRect, SpinButton::eDown, params);
+ break;
+ }
+ case Widget::eSegment: {
+ SegmentParams params = aWidgetInfo.Params<SegmentParams>();
+ DrawSegment(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSeparator: {
+ HIThemeSeparatorDrawInfo sdi = {0, kThemeStateActive};
+ HIThemeDrawSeparator(&macRect, &sdi, cgContext, HITHEME_ORIENTATION);
+ break;
+ }
+ case Widget::eToolbar: {
+ bool isMain = aWidgetInfo.Params<bool>();
+ DrawToolbar(cgContext, macRect, isMain);
+ break;
+ }
+ case Widget::eStatusBar: {
+ bool isMain = aWidgetInfo.Params<bool>();
+ DrawStatusBar(cgContext, macRect, isMain);
+ break;
+ }
+ case Widget::eGroupBox: {
+ HIThemeGroupBoxDrawInfo gdi = {0, kThemeStateActive, kHIThemeGroupBoxKindPrimary};
+ HIThemeDrawGroupBox(&macRect, &gdi, cgContext, HITHEME_ORIENTATION);
+ break;
+ }
+ case Widget::eTextField: {
+ TextFieldParams params = aWidgetInfo.Params<TextFieldParams>();
+ DrawTextField(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSearchField: {
+ TextFieldParams params = aWidgetInfo.Params<TextFieldParams>();
+ DrawSearchField(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eProgressBar: {
+ ProgressParams params = aWidgetInfo.Params<ProgressParams>();
+ DrawProgress(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMeter: {
+ MeterParams params = aWidgetInfo.Params<MeterParams>();
+ DrawMeter(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eTreeHeaderCell: {
+ TreeHeaderCellParams params = aWidgetInfo.Params<TreeHeaderCellParams>();
+ DrawTreeHeaderCell(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eScale: {
+ ScaleParams params = aWidgetInfo.Params<ScaleParams>();
+ DrawScale(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMultilineTextField: {
+ bool isFocused = aWidgetInfo.Params<bool>();
+ DrawMultilineTextField(cgContext, macRect, isFocused);
+ break;
+ }
+ case Widget::eListBox: {
+ // Fill the content with the control background color.
+ CGContextSetFillColorWithColor(cgContext, [NSColor.controlBackgroundColor CGColor]);
+ CGContextFillRect(cgContext, macRect);
+ // Draw the frame using kCUIWidgetScrollViewFrame. This is what NSScrollView uses in
+ // -[NSScrollView drawRect:] if you give it a borderType of NSBezelBorder.
+ RenderWithCoreUI(
+ macRect, cgContext, @{
+ @"widget" : @"kCUIWidgetScrollViewFrame",
+ @"kCUIIsFlippedKey" : @YES,
+ @"kCUIVariantMetal" : @NO,
+ });
+ break;
+ }
+ case Widget::eActiveSourceListSelection:
+ case Widget::eInactiveSourceListSelection: {
+ bool isInActiveWindow = aWidgetInfo.Params<bool>();
+ bool isActiveSelection = aWidgetInfo.Widget() == Widget::eActiveSourceListSelection;
+ DrawSourceListSelection(cgContext, macRect, isInActiveWindow, isActiveSelection);
+ break;
+ }
+ case Widget::eTabPanel: {
+ bool isInsideActiveWindow = aWidgetInfo.Params<bool>();
+ DrawTabPanel(cgContext, macRect, isInsideActiveWindow);
+ break;
+ }
+ }
+
+ // Reset the base CTM.
+ CGContextSetBaseCTM(cgContext, CGAffineTransformIdentity);
+
+ nativeDrawing.EndNativeDrawing();
+ }
+ }
+}
+
+bool nsNativeThemeCocoa::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (IsWidgetNonNative(aAppearance)) {
+ return ThemeCocoa::CreateWebRenderCommandsForWidget(aBuilder, aResources, aSc, aManager, aFrame,
+ aAppearance, aRect);
+ }
+
+ // This list needs to stay consistent with the list in DrawWidgetBackground.
+ // For every switch case in DrawWidgetBackground, there are three choices:
+ // - If the case in DrawWidgetBackground draws nothing for the given widget
+ // type, then don't list it here. We will hit the "default: return true;"
+ // case.
+ // - If the case in DrawWidgetBackground draws something simple for the given
+ // widget type, imitate that drawing using WebRender commands.
+ // - If the case in DrawWidgetBackground draws something complicated for the
+ // given widget type, return false here.
+ switch (aAppearance) {
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Menuseparator:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Groupbox:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Range:
+ return false;
+
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanels:
+ return false;
+
+ default:
+ return true;
+ }
+}
+
+LayoutDeviceIntMargin nsNativeThemeCocoa::DirectionAwareMargin(const LayoutDeviceIntMargin& aMargin,
+ nsIFrame* aFrame) {
+ // Assuming aMargin was originally specified for a horizontal LTR context,
+ // reinterpret the values as logical, and then map to physical coords
+ // according to aFrame's actual writing mode.
+ WritingMode wm = aFrame->GetWritingMode();
+ nsMargin m = LogicalMargin(wm, aMargin.top, aMargin.right, aMargin.bottom, aMargin.left)
+ .GetPhysicalMargin(wm);
+ return LayoutDeviceIntMargin(m.top, m.right, m.bottom, m.left);
+}
+
+static const LayoutDeviceIntMargin kAquaDropdownBorder(1, 22, 2, 5);
+static const LayoutDeviceIntMargin kAquaComboboxBorder(3, 20, 3, 4);
+static const LayoutDeviceIntMargin kAquaSearchfieldBorder(3, 5, 2, 19);
+static const LayoutDeviceIntMargin kAquaSearchfieldBorderBigSur(5, 5, 4, 26);
+
+LayoutDeviceIntMargin nsNativeThemeCocoa::GetWidgetBorder(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ LayoutDeviceIntMargin result;
+
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ if (IsButtonTypeMenu(aFrame)) {
+ result = DirectionAwareMargin(kAquaDropdownBorder, aFrame);
+ } else {
+ result = DirectionAwareMargin(LayoutDeviceIntMargin(1, 7, 3, 7), aFrame);
+ }
+ break;
+ }
+
+ case StyleAppearance::Toolbarbutton: {
+ result = DirectionAwareMargin(LayoutDeviceIntMargin(1, 4, 1, 4), aFrame);
+ break;
+ }
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ // nsCheckboxRadioFrame::GetIntrinsicWidth and nsCheckboxRadioFrame::GetIntrinsicHeight
+ // assume a border width of 2px.
+ result.SizeTo(2, 2, 2, 2);
+ break;
+ }
+
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ result = DirectionAwareMargin(kAquaDropdownBorder, aFrame);
+ break;
+
+ case StyleAppearance::Menuarrow:
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+ result.SizeTo(0, 0, 0, 28);
+ }
+ break;
+
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield: {
+ SInt32 frameOutset = 0;
+ ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset);
+
+ SInt32 textPadding = 0;
+ ::GetThemeMetric(kThemeMetricEditTextWhitespace, &textPadding);
+
+ frameOutset += textPadding;
+
+ result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset);
+ break;
+ }
+
+ case StyleAppearance::Textarea:
+ result.SizeTo(1, 1, 1, 1);
+ break;
+
+ case StyleAppearance::Searchfield: {
+ auto border = nsCocoaFeatures::OnBigSurOrLater() ? kAquaSearchfieldBorderBigSur
+ : kAquaSearchfieldBorder;
+ result = DirectionAwareMargin(border, aFrame);
+ break;
+ }
+
+ case StyleAppearance::Listbox: {
+ SInt32 frameOutset = 0;
+ ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset);
+ result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset);
+ break;
+ }
+
+ case StyleAppearance::Statusbar:
+ result.SizeTo(1, 0, 0, 0);
+ break;
+
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aContext)) {
+ result = result + result; // doubled
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(result);
+}
+
+// Return false here to indicate that CSS padding values should be used. There is
+// no reason to make a distinction between padding and border values, just specify
+// whatever values you want in GetWidgetBorder and only use this to return true
+// if you want to override CSS padding values.
+bool nsNativeThemeCocoa::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ // We don't want CSS padding being used for certain widgets.
+ // See bug 381639 for an example of why.
+ switch (aAppearance) {
+ // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
+ // and have a meaningful baseline, so they can't have
+ // author-specified padding.
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ aResult->SizeTo(0, 0, 0, 0);
+ return true;
+
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::Searchfield:
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ return false;
+}
+
+bool nsNativeThemeCocoa::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance, nsRect* aOverflowRect) {
+ if (IsWidgetNonNative(aAppearance)) {
+ return ThemeCocoa::GetWidgetOverflow(aContext, aFrame, aAppearance, aOverflowRect);
+ }
+ nsIntMargin overflow;
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Tab: {
+ overflow.SizeTo(kMaxFocusRingWidth, kMaxFocusRingWidth, kMaxFocusRingWidth,
+ kMaxFocusRingWidth);
+ break;
+ }
+ case StyleAppearance::ProgressBar: {
+ // Progress bars draw a 2 pixel white shadow under their progress indicators.
+ overflow.bottom = 2;
+ break;
+ }
+ case StyleAppearance::Meter: {
+ // Meter bars overflow their boxes by about 2 pixels.
+ overflow.SizeTo(2, 2, 2, 2);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aContext)) {
+ // Double the number of device pixels.
+ overflow += overflow;
+ }
+
+ if (overflow != nsIntMargin()) {
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+ aOverflowRect->Inflate(nsMargin(
+ NSIntPixelsToAppUnits(overflow.top, p2a), NSIntPixelsToAppUnits(overflow.right, p2a),
+ NSIntPixelsToAppUnits(overflow.bottom, p2a), NSIntPixelsToAppUnits(overflow.left, p2a)));
+ return true;
+ }
+
+ return false;
+}
+
+LayoutDeviceIntSize nsNativeThemeCocoa::GetMinimumWidgetSize(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsWidgetNonNative(aAppearance)) {
+ return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+ }
+
+ LayoutDeviceIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ result.SizeTo(pushButtonSettings.minimumSizes[miniControlSize].width,
+ pushButtonSettings.naturalSizes[miniControlSize].height);
+ break;
+ }
+
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown: {
+ result.SizeTo(kMenuScrollArrowSize.width, kMenuScrollArrowSize.height);
+ break;
+ }
+
+ case StyleAppearance::Menuarrow: {
+ result.SizeTo(kMenuarrowSize.width, kMenuarrowSize.height);
+ break;
+ }
+
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed: {
+ result.SizeTo(kDisclosureButtonSize.width, kDisclosureButtonSize.height);
+ break;
+ }
+
+ case StyleAppearance::MozMacHelpButton: {
+ result.SizeTo(kHelpButtonSize.width, kHelpButtonSize.height);
+ break;
+ }
+
+ case StyleAppearance::Toolbarbutton: {
+ result.SizeTo(0, toolbarButtonHeights[miniControlSize]);
+ break;
+ }
+
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton: {
+ SInt32 buttonHeight = 0, buttonWidth = 0;
+ if (aFrame->GetContent()->IsXULElement()) {
+ ::GetThemeMetric(kThemeMetricLittleArrowsWidth, &buttonWidth);
+ ::GetThemeMetric(kThemeMetricLittleArrowsHeight, &buttonHeight);
+ } else {
+ NSSize size = spinnerSettings.minimumSizes[EnumSizeForCocoaSize(NSControlSizeMini)];
+ buttonWidth = size.width;
+ buttonHeight = size.height;
+ if (aAppearance != StyleAppearance::Spinner) {
+ // the buttons are half the height of the spinner
+ buttonHeight /= 2;
+ }
+ }
+ result.SizeTo(buttonWidth, buttonHeight);
+ break;
+ }
+
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton: {
+ SInt32 popupHeight = 0;
+ ::GetThemeMetric(kThemeMetricPopupButtonHeight, &popupHeight);
+ result.SizeTo(0, popupHeight);
+ break;
+ }
+
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield: {
+ // at minimum, we should be tall enough for 9pt text.
+ // I'm using hardcoded values here because the appearance manager
+ // values for the frame size are incorrect.
+ result.SizeTo(0, (2 + 2) /* top */ + 9 + (1 + 1) /* bottom */);
+ break;
+ }
+
+ case StyleAppearance::MozWindowButtonBox: {
+ NSSize size = WindowButtonsSize(aFrame);
+ result.SizeTo(size.width, size.height);
+ break;
+ }
+
+ case StyleAppearance::ProgressBar: {
+ SInt32 barHeight = 0;
+ ::GetThemeMetric(kThemeMetricNormalProgressBarThickness, &barHeight);
+ result.SizeTo(0, barHeight);
+ break;
+ }
+
+ case StyleAppearance::Separator: {
+ result.SizeTo(1, 1);
+ break;
+ }
+
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen: {
+ SInt32 twistyHeight = 0, twistyWidth = 0;
+ ::GetThemeMetric(kThemeMetricDisclosureButtonWidth, &twistyWidth);
+ ::GetThemeMetric(kThemeMetricDisclosureButtonHeight, &twistyHeight);
+ result.SizeTo(twistyWidth, twistyHeight);
+ break;
+ }
+
+ case StyleAppearance::Treeheader:
+ case StyleAppearance::Treeheadercell: {
+ SInt32 headerHeight = 0;
+ ::GetThemeMetric(kThemeMetricListHeaderHeight, &headerHeight);
+ result.SizeTo(0, headerHeight);
+ break;
+ }
+
+ case StyleAppearance::Tab: {
+ result.SizeTo(0, tabHeights[miniControlSize]);
+ break;
+ }
+
+ case StyleAppearance::RangeThumb: {
+ SInt32 width = 0;
+ SInt32 height = 0;
+ ::GetThemeMetric(kThemeMetricSliderMinThumbWidth, &width);
+ ::GetThemeMetric(kThemeMetricSliderMinThumbHeight, &height);
+ result.SizeTo(width, height);
+ break;
+ }
+
+ case StyleAppearance::MozMenulistArrowButton:
+ return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aPresContext->DeviceContext())) {
+ result = result * 2;
+ }
+
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntSize());
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ // Some widget types just never change state.
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Dialog:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Groupbox:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Meterchunk:
+ *aShouldRepaint = false;
+ return NS_OK;
+ default:
+ break;
+ }
+
+ // XXXdwh Not sure what can really be done here. Can at least guess for
+ // specific widgets that they're highly unlikely to have certain states.
+ // For example, a toolbar doesn't care about any states.
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ } else {
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected || aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::sortDirection ||
+ aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default ||
+ aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover)
+ *aShouldRepaint = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::ThemeChanged() {
+ // This is unimplemented because we don't care if gecko changes its theme
+ // and macOS system appearance changes are handled by
+ // nsLookAndFeel::SystemWantsDarkTheme.
+ return NS_OK;
+}
+
+bool nsNativeThemeCocoa::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aAppearance)) {
+ return ThemeCocoa::ThemeSupportsWidget(aPresContext, aFrame, aAppearance);
+ }
+ // if this is a dropdown button in a combobox the answer is always no
+ if (aAppearance == StyleAppearance::MozMenulistArrowButton) {
+ nsIFrame* parentFrame = aFrame->GetParent();
+ if (parentFrame && parentFrame->IsComboboxControlFrame()) return false;
+ }
+
+ switch (aAppearance) {
+ // Combobox dropdowns don't support native theming in vertical mode.
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::MenulistText:
+ if (aFrame && aFrame->GetWritingMode().IsVertical()) {
+ return false;
+ }
+ [[fallthrough]];
+
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Dialog:
+ case StyleAppearance::Window:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Menuseparator:
+ case StyleAppearance::Tooltip:
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::CheckboxContainer:
+ case StyleAppearance::Radio:
+ case StyleAppearance::RadioContainer:
+ case StyleAppearance::Groupbox:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::Button:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Meterchunk:
+ case StyleAppearance::Separator:
+
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Tab:
+
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treeheader:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeline:
+ case StyleAppearance::MozMacSourceList:
+ case StyleAppearance::MozMacSourceListSelection:
+ case StyleAppearance::MozMacActiveSourceListSelection:
+
+ case StyleAppearance::Range:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool nsNativeThemeCocoa::WidgetIsContainer(StyleAppearance aAppearance) {
+ // flesh this out at some point
+ switch (aAppearance) {
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Range:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+bool nsNativeThemeCocoa::ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Range:
+ case StyleAppearance::Checkbox:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool nsNativeThemeCocoa::ThemeNeedsComboboxDropmarker() { return false; }
+
+bool nsNativeThemeCocoa::WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Dialog:
+ case StyleAppearance::Groupbox:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Menuseparator:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treeline:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Listbox:
+ return false;
+ default:
+ return true;
+ }
+}
+
+nsITheme::ThemeGeometryType nsNativeThemeCocoa::ThemeGeometryTypeForWidget(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowTitlebar:
+ return eThemeGeometryTypeTitlebar;
+ case StyleAppearance::Toolbar:
+ return eThemeGeometryTypeToolbar;
+ case StyleAppearance::Toolbox:
+ return eThemeGeometryTypeToolbox;
+ case StyleAppearance::MozWindowButtonBox:
+ return eThemeGeometryTypeWindowButtons;
+ case StyleAppearance::Tooltip:
+ return eThemeGeometryTypeTooltip;
+ case StyleAppearance::Menupopup:
+ return eThemeGeometryTypeMenu;
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem: {
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ bool isDisabled = elementState.HasState(ElementState::DISABLED);
+ bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive);
+ return isSelected ? eThemeGeometryTypeHighlightedMenuItem : eThemeGeometryTypeMenu;
+ }
+ case StyleAppearance::MozMacSourceList:
+ return eThemeGeometryTypeSourceList;
+ case StyleAppearance::MozMacSourceListSelection:
+ return IsInSourceList(aFrame) ? eThemeGeometryTypeSourceListSelection
+ : eThemeGeometryTypeUnknown;
+ case StyleAppearance::MozMacActiveSourceListSelection:
+ return IsInSourceList(aFrame) ? eThemeGeometryTypeActiveSourceListSelection
+ : eThemeGeometryTypeUnknown;
+ default:
+ return eThemeGeometryTypeUnknown;
+ }
+}
+
+nsITheme::Transparency nsNativeThemeCocoa::GetWidgetTransparency(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetScrollbarPart(aAppearance)) {
+ return ThemeCocoa::GetWidgetTransparency(aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Dialog:
+ case StyleAppearance::Toolbar:
+ return eTransparent;
+
+ case StyleAppearance::Statusbar:
+ // Knowing that scrollbars and statusbars are opaque improves
+ // performance, because we create layers for them.
+ return eOpaque;
+
+ default:
+ return eUnknownTransparency;
+ }
+}
+
+already_AddRefed<widget::Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ return do_AddRef(new nsNativeThemeCocoa());
+}
diff --git a/widget/cocoa/nsNativeThemeColors.h b/widget/cocoa/nsNativeThemeColors.h
new file mode 100644
index 0000000000..ba51943aef
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeColors.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsNativeThemeColors_h_
+#define nsNativeThemeColors_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsCocoaFeatures.h"
+#include "SDKDeclarations.h"
+#include "mozilla/ColorScheme.h"
+
+enum ColorName {
+ toolbarTopBorderGrey,
+ toolbarFillGrey,
+ toolbarBottomBorderGrey,
+};
+
+static const int sLionThemeColors[][2] = {
+ /* { active window, inactive window } */
+ // toolbar:
+ {0xD0, 0xF0}, // top separator line
+ {0xB2, 0xE1}, // fill color
+ {0x59, 0x87}, // bottom separator line
+};
+
+static const int sYosemiteThemeColors[][2] = {
+ /* { active window, inactive window } */
+ // toolbar:
+ {0xBD, 0xDF}, // top separator line
+ {0xD3, 0xF6}, // fill color
+ {0xB3, 0xD1}, // bottom separator line
+};
+
+inline int NativeGreyColorAsInt(ColorName name, BOOL isMain) {
+ return sYosemiteThemeColors[name][isMain ? 0 : 1];
+}
+
+inline float NativeGreyColorAsFloat(ColorName name, BOOL isMain) {
+ return NativeGreyColorAsInt(name, isMain) / 255.0f;
+}
+
+inline void DrawNativeGreyColorInRect(CGContextRef context, ColorName name, CGRect rect,
+ BOOL isMain) {
+ float grey = NativeGreyColorAsFloat(name, isMain);
+ CGContextSetRGBFillColor(context, grey, grey, grey, 1.0f);
+ CGContextFillRect(context, rect);
+}
+
+inline NSColor* ControlAccentColor() {
+ if (@available(macOS 10.14, *)) {
+ return [NSColor controlAccentColor];
+ }
+
+ // Pre-10.14, use hardcoded colors.
+ return [NSColor currentControlTint] == NSGraphiteControlTint
+ ? [NSColor colorWithSRGBRed:0.635 green:0.635 blue:0.655 alpha:1.0]
+ : [NSColor colorWithSRGBRed:0.247 green:0.584 blue:0.965 alpha:1.0];
+}
+
+inline NSAppearance* NSAppearanceForColorScheme(mozilla::ColorScheme aScheme) {
+ if (@available(macOS 10.14, *)) {
+ NSAppearanceName appearanceName =
+ aScheme == mozilla::ColorScheme::Light ? NSAppearanceNameAqua : NSAppearanceNameDarkAqua;
+ return [NSAppearance appearanceNamed:appearanceName];
+ }
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+}
+
+#endif // nsNativeThemeColors_h_
diff --git a/widget/cocoa/nsPIWidgetCocoa.idl b/widget/cocoa/nsPIWidgetCocoa.idl
new file mode 100644
index 0000000000..54aa0d5113
--- /dev/null
+++ b/widget/cocoa/nsPIWidgetCocoa.idl
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIWidget;
+
+[ptr] native NSWindowPtr(NSWindow);
+
+//
+// nsPIWidgetCocoa
+//
+// A private interface (unfrozen, private to the widget implementation) that
+// gives us access to some extra features on a widget/window.
+//
+[uuid(f75ff69e-3a51-419e-bd29-042f804bc2ed)]
+interface nsPIWidgetCocoa : nsISupports
+{
+ void SendSetZLevelEvent();
+
+ // Find the displayed child sheet (if aShown) or a child sheet that
+ // wants to be displayed (if !aShown)
+ nsIWidget GetChildSheet(in boolean aShown);
+
+ // Get the parent widget (if any) StandardCreate() was called with.
+ nsIWidget GetRealParent();
+
+ // If the object implementing this interface is a sheet, this will return the
+ // native NSWindow it is attached to
+ readonly attribute NSWindowPtr sheetWindowParent;
+
+ // True if window is a sheet
+ readonly attribute boolean isSheet;
+
+}; // nsPIWidgetCocoa
diff --git a/widget/cocoa/nsPrintDialogX.h b/widget/cocoa/nsPrintDialogX.h
new file mode 100644
index 0000000000..ed8c7dbb05
--- /dev/null
+++ b/widget/cocoa/nsPrintDialogX.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsPrintDialog_h_
+#define nsPrintDialog_h_
+
+#include "nsIPrintDialogService.h"
+#include "nsCOMPtr.h"
+#include "nsCocoaUtils.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsIPrintSettings;
+class nsIStringBundle;
+
+class nsPrintDialogServiceX final : public nsIPrintDialogService {
+ virtual ~nsPrintDialogServiceX();
+
+ public:
+ nsPrintDialogServiceX();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTDIALOGSERVICE
+};
+
+@interface PrintPanelAccessoryView : NSView {
+ nsIPrintSettings* mSettings;
+ nsIStringBundle* mPrintBundle;
+ NSButton* mPrintSelectionOnlyCheckbox;
+ NSButton* mShrinkToFitCheckbox;
+ NSButton* mPrintBGColorsCheckbox;
+ NSButton* mPrintBGImagesCheckbox;
+ NSPopUpButton* mHeaderLeftList;
+ NSPopUpButton* mHeaderCenterList;
+ NSPopUpButton* mHeaderRightList;
+ NSPopUpButton* mFooterLeftList;
+ NSPopUpButton* mFooterCenterList;
+ NSPopUpButton* mFooterRightList;
+}
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings haveSelection:(bool)aHaveSelection;
+
+- (void)exportSettings;
+
+@end
+
+@interface PrintPanelAccessoryController : NSViewController <NSPrintPanelAccessorizing>
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings haveSelection:(bool)aHaveSelection;
+
+- (void)exportSettings;
+
+@end
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceX, NS_IPRINTDIALOGSERVICE_IID)
+
+#endif // nsPrintDialog_h_
diff --git a/widget/cocoa/nsPrintDialogX.mm b/widget/cocoa/nsPrintDialogX.mm
new file mode 100644
index 0000000000..9a087912a6
--- /dev/null
+++ b/widget/cocoa/nsPrintDialogX.mm
@@ -0,0 +1,590 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/gfx/PrintTargetCG.h"
+#include "mozilla/Preferences.h"
+
+#include "nsPrintDialogX.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsService.h"
+#include "nsPrintSettingsX.h"
+#include "nsCOMPtr.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIStringBundle.h"
+#include "nsCRT.h"
+
+#import <Cocoa/Cocoa.h>
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::gfx::PrintTarget;
+
+NS_IMPL_ISUPPORTS(nsPrintDialogServiceX, nsIPrintDialogService)
+
+// Splits our single pages-per-sheet count for native NSPrintInfo:
+static void setPagesPerSheet(NSPrintInfo* aPrintInfo, int32_t aPPS) {
+ int32_t across, down;
+ // Assumes portrait - we'll swap if landscape.
+ switch (aPPS) {
+ case 2:
+ across = 1;
+ down = 2;
+ break;
+ case 4:
+ across = 2;
+ down = 2;
+ break;
+ case 6:
+ across = 2;
+ down = 3;
+ break;
+ case 9:
+ across = 3;
+ down = 3;
+ break;
+ case 16:
+ across = 4;
+ down = 4;
+ break;
+ default:
+ across = 1;
+ down = 1;
+ break;
+ }
+ if ([aPrintInfo orientation] == NSPaperOrientationLandscape) {
+ std::swap(across, down);
+ }
+
+ NSMutableDictionary* dict = [aPrintInfo dictionary];
+
+ [dict setObject:[NSNumber numberWithInt:across] forKey:@"NSPagesAcross"];
+ [dict setObject:[NSNumber numberWithInt:down] forKey:@"NSPagesDown"];
+}
+
+nsPrintDialogServiceX::nsPrintDialogServiceX() {}
+
+nsPrintDialogServiceX::~nsPrintDialogServiceX() {}
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::ShowPrintDialog(mozIDOMWindowProxy* aParent, bool aHaveSelection,
+ nsIPrintSettings* aSettings) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aSettings, "aSettings must not be null");
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings));
+ if (!settingsX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPrintInfo* printInfo = settingsX->CreateOrCopyPrintInfo(/* aWithScaling = */ true);
+ if (NS_WARN_IF(!printInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+ [printInfo autorelease];
+
+ // Set the print job title
+ nsAutoString docName;
+ nsresult rv = aSettings->GetTitle(docName);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString adjustedTitle;
+ PrintTarget::AdjustPrintJobNameForIPP(docName, adjustedTitle);
+ CFStringRef cfTitleString = CFStringCreateWithCharacters(
+ NULL, reinterpret_cast<const UniChar*>(adjustedTitle.BeginReading()),
+ adjustedTitle.Length());
+ if (cfTitleString) {
+ auto pmPrintSettings = static_cast<PMPrintSettings>([printInfo PMPrintSettings]);
+ ::PMPrintSettingsSetJobName(pmPrintSettings, cfTitleString);
+ [printInfo updateFromPMPrintSettings];
+ CFRelease(cfTitleString);
+ }
+ }
+
+ // Temporarily set the pages-per-sheet count set in our print preview to
+ // pre-populate the system dialog with the same value:
+ int32_t pagesPerSheet;
+ aSettings->GetNumPagesPerSheet(&pagesPerSheet);
+ setPagesPerSheet(printInfo, pagesPerSheet);
+
+ // Put the print info into the current print operation, since that's where
+ // [panel runModal] will look for it. We create the view because otherwise
+ // we'll get unrelated warnings printed to the console.
+ NSView* tmpView = [[NSView alloc] init];
+ NSPrintOperation* printOperation = [NSPrintOperation printOperationWithView:tmpView
+ printInfo:printInfo];
+ [NSPrintOperation setCurrentOperation:printOperation];
+
+ NSPrintPanel* panel = [NSPrintPanel printPanel];
+ [panel setOptions:NSPrintPanelShowsCopies | NSPrintPanelShowsPageRange |
+ NSPrintPanelShowsPaperSize | NSPrintPanelShowsOrientation |
+ NSPrintPanelShowsScaling];
+ PrintPanelAccessoryController* viewController =
+ [[PrintPanelAccessoryController alloc] initWithSettings:aSettings
+ haveSelection:aHaveSelection];
+ [panel addAccessoryController:viewController];
+ [viewController release];
+
+ // Show the dialog.
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int button = [panel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ // Retrieve a printInfo with the updated settings. (The NSPrintOperation operates on a
+ // copy, so the object we passed in will not have been modified.)
+ NSPrintInfo* result = [[NSPrintOperation currentOperation] printInfo];
+ if (!result) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [NSPrintOperation setCurrentOperation:nil];
+ [tmpView release];
+
+ if (button != NSFileHandlingPanelOKButton) {
+ return NS_ERROR_ABORT;
+ }
+
+ // We handle pages-per-sheet internally and we want to prevent the macOS
+ // printing code from also applying the pages-per-sheet count. So we need
+ // to move the count off the NSPrintInfo and over to the nsIPrintSettings.
+ NSMutableDictionary* dict = [result dictionary];
+ auto pagesAcross = [[dict objectForKey:@"NSPagesAcross"] intValue];
+ auto pagesDown = [[dict objectForKey:@"NSPagesDown"] intValue];
+ [dict setObject:[NSNumber numberWithUnsignedInt:1] forKey:@"NSPagesAcross"];
+ [dict setObject:[NSNumber numberWithUnsignedInt:1] forKey:@"NSPagesDown"];
+ aSettings->SetNumPagesPerSheet(pagesAcross * pagesDown);
+
+ // Export settings.
+ [viewController exportSettings];
+
+ // Update our settings object based on the user's choices in the dialog.
+ // We tell settingsX to adopt this printInfo so that it will be used to run print job,
+ // so that any printer-specific custom settings from print dialog extension panels
+ // will be carried through.
+ settingsX->SetFromPrintInfo(result, /* aAdoptPrintInfo = */ true);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::ShowPageSetupDialog(mozIDOMWindowProxy* aParent,
+ nsIPrintSettings* aNSSettings) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aParent, "aParent must not be null");
+ MOZ_ASSERT(aNSSettings, "aSettings must not be null");
+ NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE);
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aNSSettings));
+ if (!settingsX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPrintInfo* printInfo = settingsX->CreateOrCopyPrintInfo(/* aWithScaling = */ true);
+ if (NS_WARN_IF(!printInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+ [printInfo autorelease];
+
+ NSPageLayout* pageLayout = [NSPageLayout pageLayout];
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int button = [pageLayout runModalWithPrintInfo:printInfo];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (button == NSFileHandlingPanelOKButton) {
+ // The Page Setup dialog does not include non-standard settings that need to be preserved,
+ // separate from what the base printSettings object handles, so we do not need it to adopt
+ // the printInfo object here.
+ settingsX->SetFromPrintInfo(printInfo, /* aAdoptPrintInfo = */ false);
+ nsCOMPtr<nsIPrintSettingsService> printSettingsService =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (printSettingsService && Preferences::GetBool("print.save_print_settings", false)) {
+ uint32_t flags = nsIPrintSettings::kInitSavePaperSize |
+ nsIPrintSettings::kInitSaveOrientation | nsIPrintSettings::kInitSaveScaling;
+ printSettingsService->MaybeSavePrintSettingsToPrefs(aNSSettings, flags);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_ABORT;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// Accessory view
+
+@interface PrintPanelAccessoryView (Private)
+
+- (NSString*)localizedString:(const char*)aKey;
+
+- (const char*)headerFooterStringForList:(NSPopUpButton*)aList;
+
+- (void)exportHeaderFooterSettings;
+
+- (void)initBundle;
+
+- (NSTextField*)label:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment;
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect alignment:(NSTextAlignment)aAlignment;
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect;
+
+- (void)addCenteredLabel:(const char*)aLabel withFrame:(NSRect)aRect;
+
+- (NSButton*)checkboxWithLabel:(const char*)aLabel andFrame:(NSRect)aRect;
+
+- (NSPopUpButton*)headerFooterItemListWithFrame:(NSRect)aRect
+ selectedItem:(const nsAString&)aCurrentString;
+
+- (void)addOptionsSection:(bool)aHaveSelection;
+
+- (void)addAppearanceSection;
+
+- (void)addHeaderFooterSection;
+
+- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox;
+
+- (NSString*)headerSummaryValue;
+
+- (NSString*)footerSummaryValue;
+
+@end
+
+static const char sHeaderFooterTags[][4] = {"", "&T", "&U", "&D", "&P", "&PT"};
+
+@implementation PrintPanelAccessoryView
+
+// Public methods
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings haveSelection:(bool)aHaveSelection {
+ [super initWithFrame:NSMakeRect(0, 0, 540, 185)];
+
+ mSettings = aSettings;
+ [self initBundle];
+ [self addOptionsSection:aHaveSelection];
+ [self addAppearanceSection];
+ [self addHeaderFooterSection];
+
+ return self;
+}
+
+- (void)exportSettings {
+ mSettings->SetPrintSelectionOnly([mPrintSelectionOnlyCheckbox state] == NSOnState);
+ mSettings->SetShrinkToFit([mShrinkToFitCheckbox state] == NSOnState);
+ mSettings->SetPrintBGColors([mPrintBGColorsCheckbox state] == NSOnState);
+ mSettings->SetPrintBGImages([mPrintBGImagesCheckbox state] == NSOnState);
+
+ [self exportHeaderFooterSettings];
+}
+
+- (void)dealloc {
+ NS_IF_RELEASE(mPrintBundle);
+ [super dealloc];
+}
+
+// Localization
+
+- (void)initBundle {
+ nsCOMPtr<nsIStringBundleService> bundleSvc = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties", &mPrintBundle);
+}
+
+- (NSString*)localizedString:(const char*)aKey {
+ if (!mPrintBundle) return @"";
+
+ nsAutoString intlString;
+ mPrintBundle->GetStringFromName(aKey, intlString);
+ NSMutableString* s =
+ [NSMutableString stringWithUTF8String:NS_ConvertUTF16toUTF8(intlString).get()];
+
+ // Remove all underscores (they're used in the GTK dialog for accesskeys).
+ [s replaceOccurrencesOfString:@"_" withString:@"" options:0 range:NSMakeRange(0, [s length])];
+ return s;
+}
+
+// Widget helpers
+
+- (NSTextField*)label:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment {
+ NSTextField* label = [[[NSTextField alloc] initWithFrame:aRect] autorelease];
+ [label setStringValue:[self localizedString:aLabel]];
+ [label setEditable:NO];
+ [label setSelectable:NO];
+ [label setBezeled:NO];
+ [label setBordered:NO];
+ [label setDrawsBackground:NO];
+ [label setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ [label setAlignment:aAlignment];
+ return label;
+}
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect alignment:(NSTextAlignment)aAlignment {
+ NSTextField* label = [self label:aLabel withFrame:aRect alignment:aAlignment];
+ [self addSubview:label];
+}
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect {
+ [self addLabel:aLabel withFrame:aRect alignment:NSTextAlignmentRight];
+}
+
+- (void)addCenteredLabel:(const char*)aLabel withFrame:(NSRect)aRect {
+ [self addLabel:aLabel withFrame:aRect alignment:NSTextAlignmentCenter];
+}
+
+- (NSButton*)checkboxWithLabel:(const char*)aLabel andFrame:(NSRect)aRect {
+ aRect.origin.y += 4.0f;
+ NSButton* checkbox = [[[NSButton alloc] initWithFrame:aRect] autorelease];
+ [checkbox setButtonType:NSSwitchButton];
+ [checkbox setTitle:[self localizedString:aLabel]];
+ [checkbox setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ [checkbox sizeToFit];
+ return checkbox;
+}
+
+- (NSPopUpButton*)headerFooterItemListWithFrame:(NSRect)aRect
+ selectedItem:(const nsAString&)aCurrentString {
+ NSPopUpButton* list = [[[NSPopUpButton alloc] initWithFrame:aRect pullsDown:NO] autorelease];
+ [list setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [[list cell] setControlSize:NSControlSizeSmall];
+ NSArray* items = [NSArray arrayWithObjects:[self localizedString:"headerFooterBlank"],
+ [self localizedString:"headerFooterTitle"],
+ [self localizedString:"headerFooterURL"],
+ [self localizedString:"headerFooterDate"],
+ [self localizedString:"headerFooterPage"],
+ [self localizedString:"headerFooterPageTotal"], nil];
+ [list addItemsWithTitles:items];
+
+ NS_ConvertUTF16toUTF8 currentStringUTF8(aCurrentString);
+ for (unsigned int i = 0; i < ArrayLength(sHeaderFooterTags); i++) {
+ if (!strcmp(currentStringUTF8.get(), sHeaderFooterTags[i])) {
+ [list selectItemAtIndex:i];
+ break;
+ }
+ }
+
+ return list;
+}
+
+// Build sections
+
+- (void)addOptionsSection:(bool)aHaveSelection {
+ // Title
+ [self addLabel:"optionsTitleMac" withFrame:NSMakeRect(0, 155, 151, 22)];
+
+ // "Print Selection Only"
+ mPrintSelectionOnlyCheckbox = [self checkboxWithLabel:"selectionOnly"
+ andFrame:NSMakeRect(156, 155, 0, 0)];
+ [mPrintSelectionOnlyCheckbox setEnabled:aHaveSelection];
+
+ if (mSettings->GetPrintSelectionOnly()) {
+ [mPrintSelectionOnlyCheckbox setState:NSOnState];
+ }
+
+ [self addSubview:mPrintSelectionOnlyCheckbox];
+
+ // "Shrink To Fit"
+ mShrinkToFitCheckbox = [self checkboxWithLabel:"shrinkToFit" andFrame:NSMakeRect(156, 133, 0, 0)];
+
+ bool shrinkToFit;
+ mSettings->GetShrinkToFit(&shrinkToFit);
+ [mShrinkToFitCheckbox setState:(shrinkToFit ? NSOnState : NSOffState)];
+
+ [self addSubview:mShrinkToFitCheckbox];
+}
+
+- (void)addAppearanceSection {
+ // Title
+ [self addLabel:"appearanceTitleMac" withFrame:NSMakeRect(0, 103, 151, 22)];
+
+ // "Print Background Colors"
+ mPrintBGColorsCheckbox = [self checkboxWithLabel:"printBGColors"
+ andFrame:NSMakeRect(156, 103, 0, 0)];
+
+ bool geckoBool = mSettings->GetPrintBGColors();
+ [mPrintBGColorsCheckbox setState:(geckoBool ? NSOnState : NSOffState)];
+
+ [self addSubview:mPrintBGColorsCheckbox];
+
+ // "Print Background Images"
+ mPrintBGImagesCheckbox = [self checkboxWithLabel:"printBGImages"
+ andFrame:NSMakeRect(156, 81, 0, 0)];
+
+ geckoBool = mSettings->GetPrintBGImages();
+ [mPrintBGImagesCheckbox setState:(geckoBool ? NSOnState : NSOffState)];
+
+ [self addSubview:mPrintBGImagesCheckbox];
+}
+
+- (void)addHeaderFooterSection {
+ // Labels
+ [self addLabel:"pageHeadersTitleMac" withFrame:NSMakeRect(0, 44, 151, 22)];
+ [self addLabel:"pageFootersTitleMac" withFrame:NSMakeRect(0, 0, 151, 22)];
+ [self addCenteredLabel:"left" withFrame:NSMakeRect(156, 22, 100, 22)];
+ [self addCenteredLabel:"center" withFrame:NSMakeRect(256, 22, 100, 22)];
+ [self addCenteredLabel:"right" withFrame:NSMakeRect(356, 22, 100, 22)];
+
+ // Lists
+ nsString sel;
+
+ mSettings->GetHeaderStrLeft(sel);
+ mHeaderLeftList = [self headerFooterItemListWithFrame:NSMakeRect(156, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderLeftList];
+
+ mSettings->GetHeaderStrCenter(sel);
+ mHeaderCenterList = [self headerFooterItemListWithFrame:NSMakeRect(256, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderCenterList];
+
+ mSettings->GetHeaderStrRight(sel);
+ mHeaderRightList = [self headerFooterItemListWithFrame:NSMakeRect(356, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderRightList];
+
+ mSettings->GetFooterStrLeft(sel);
+ mFooterLeftList = [self headerFooterItemListWithFrame:NSMakeRect(156, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterLeftList];
+
+ mSettings->GetFooterStrCenter(sel);
+ mFooterCenterList = [self headerFooterItemListWithFrame:NSMakeRect(256, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterCenterList];
+
+ mSettings->GetFooterStrRight(sel);
+ mFooterRightList = [self headerFooterItemListWithFrame:NSMakeRect(356, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterRightList];
+}
+
+// Export settings
+
+- (const char*)headerFooterStringForList:(NSPopUpButton*)aList {
+ NSInteger index = [aList indexOfSelectedItem];
+ NS_ASSERTION(index < NSInteger(ArrayLength(sHeaderFooterTags)),
+ "Index of dropdown is higher than expected!");
+ return sHeaderFooterTags[index];
+}
+
+- (void)exportHeaderFooterSettings {
+ const char* headerFooterStr;
+ headerFooterStr = [self headerFooterStringForList:mHeaderLeftList];
+ mSettings->SetHeaderStrLeft(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mHeaderCenterList];
+ mSettings->SetHeaderStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mHeaderRightList];
+ mSettings->SetHeaderStrRight(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterLeftList];
+ mSettings->SetFooterStrLeft(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterCenterList];
+ mSettings->SetFooterStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterRightList];
+ mSettings->SetFooterStrRight(NS_ConvertUTF8toUTF16(headerFooterStr));
+}
+
+// Summary
+
+- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox {
+ if (![aCheckbox isEnabled]) return [self localizedString:"summaryNAValue"];
+
+ return [aCheckbox state] == NSOnState ? [self localizedString:"summaryOnValue"]
+ : [self localizedString:"summaryOffValue"];
+}
+
+- (NSString*)headerSummaryValue {
+ return [[mHeaderLeftList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", "
+ stringByAppendingString:
+ [[mHeaderCenterList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", " stringByAppendingString:[mHeaderRightList titleOfSelectedItem]]]]];
+}
+
+- (NSString*)footerSummaryValue {
+ return [[mFooterLeftList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", "
+ stringByAppendingString:
+ [[mFooterCenterList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", " stringByAppendingString:[mFooterRightList titleOfSelectedItem]]]]];
+}
+
+- (NSArray*)localizedSummaryItems {
+ return [NSArray
+ arrayWithObjects:
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:[self localizedString:"summarySelectionOnlyTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self
+ summaryValueForCheckbox:mPrintSelectionOnlyCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:[self localizedString:"summaryShrinkToFitTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mShrinkToFitCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:[self localizedString:"summaryPrintBGColorsTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mPrintBGColorsCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:[self localizedString:"summaryPrintBGImagesTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mPrintBGImagesCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:[self localizedString:"summaryHeaderTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self headerSummaryValue],
+ NSPrintPanelAccessorySummaryItemDescriptionKey,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:[self localizedString:"summaryFooterTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self footerSummaryValue],
+ NSPrintPanelAccessorySummaryItemDescriptionKey,
+ nil],
+ nil];
+}
+
+@end
+
+// Accessory controller
+
+@implementation PrintPanelAccessoryController
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings haveSelection:(bool)aHaveSelection {
+ [super initWithNibName:nil bundle:nil];
+
+ NSView* accView = [[PrintPanelAccessoryView alloc] initWithSettings:aSettings
+ haveSelection:aHaveSelection];
+ [self setView:accView];
+ [accView release];
+ return self;
+}
+
+- (void)exportSettings {
+ return [(PrintPanelAccessoryView*)[self view] exportSettings];
+}
+
+- (NSArray*)localizedSummaryItems {
+ return [(PrintPanelAccessoryView*)[self view] localizedSummaryItems];
+}
+
+@end
diff --git a/widget/cocoa/nsPrintSettingsServiceX.h b/widget/cocoa/nsPrintSettingsServiceX.h
new file mode 100644
index 0000000000..e6e6bff2b9
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsServiceX.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsPrintSettingsServiceX_h
+#define nsPrintSettingsServiceX_h
+
+#include "nsPrintSettingsService.h"
+
+namespace mozilla {
+namespace embedding {
+class PrintData;
+} // namespace embedding
+} // namespace mozilla
+
+class nsPrintSettingsServiceX final : public nsPrintSettingsService {
+ public:
+ nsPrintSettingsServiceX() {}
+
+ NS_IMETHODIMP SerializeToPrintData(
+ nsIPrintSettings* aSettings,
+ mozilla::embedding::PrintData* data) override;
+
+ NS_IMETHODIMP DeserializeToPrintSettings(
+ const mozilla::embedding::PrintData& data,
+ nsIPrintSettings* settings) override;
+
+ protected:
+ nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override;
+};
+
+#endif // nsPrintSettingsServiceX_h
diff --git a/widget/cocoa/nsPrintSettingsServiceX.mm b/widget/cocoa/nsPrintSettingsServiceX.mm
new file mode 100644
index 0000000000..7598780086
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsServiceX.mm
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsPrintSettingsServiceX.h"
+
+#include "mozilla/embedding/PPrintingTypes.h"
+#include "nsCOMPtr.h"
+#include "nsQueryObject.h"
+#include "nsPrintSettingsX.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::embedding;
+
+NS_IMETHODIMP
+nsPrintSettingsServiceX::SerializeToPrintData(nsIPrintSettings* aSettings, PrintData* data) {
+ nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings));
+ if (NS_WARN_IF(!settingsX)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ settingsX->GetDisposition(data->disposition());
+ settingsX->GetDestination(&data->destination());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsServiceX::DeserializeToPrintSettings(const PrintData& data,
+ nsIPrintSettings* settings) {
+ nsresult rv = nsPrintSettingsService::DeserializeToPrintSettings(data, settings);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(settings));
+ if (NS_WARN_IF(!settingsX)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ settingsX->SetDisposition(data.disposition());
+ settingsX->SetDestination(data.destination());
+
+ return NS_OK;
+}
+
+nsresult nsPrintSettingsServiceX::_CreatePrintSettings(nsIPrintSettings** _retval) {
+ nsresult rv;
+ *_retval = nullptr;
+
+ nsPrintSettingsX* printSettings = new nsPrintSettingsX; // does not initially ref count
+ NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = printSettings);
+
+ rv = printSettings->Init();
+ if (NS_FAILED(rv)) {
+ NS_RELEASE(*_retval);
+ return rv;
+ }
+
+ auto globalPrintSettings = nsIPrintSettings::kGlobalSettings;
+
+ // XXX Why is Mac special? Why are we copying global print settings here?
+ // nsPrintSettingsService::InitPrintSettingsFromPrefs already gets the few
+ // global defaults that we want, doesn't it?
+ InitPrintSettingsFromPrefs(*_retval, false, globalPrintSettings);
+ return rv;
+}
diff --git a/widget/cocoa/nsPrintSettingsX.h b/widget/cocoa/nsPrintSettingsX.h
new file mode 100644
index 0000000000..9f24694bb3
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsX.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsPrintSettingsX_h_
+#define nsPrintSettingsX_h_
+
+#include "nsPrintSettingsImpl.h"
+#import <Cocoa/Cocoa.h>
+
+// clang-format off
+#define NS_PRINTSETTINGSX_IID \
+ { \
+ 0x0DF2FDBD, 0x906D, 0x4726, { \
+ 0x9E, 0x4D, 0xCF, 0xE0, 0x87, 0x8D, 0x70, 0x7C \
+ } \
+ }
+// clang-format on
+
+class nsPrintSettingsX : public nsPrintSettings {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_PRINTSETTINGSX_IID)
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsPrintSettingsX();
+ explicit nsPrintSettingsX(const PrintSettingsInitializer& aSettings);
+
+ nsresult Init() { return NS_OK; }
+
+ void SetDestination(uint16_t aDestination) { mDestination = aDestination; }
+ void GetDestination(uint16_t* aDestination) { *aDestination = mDestination; }
+
+ void SetDisposition(const nsString& aDisposition) { mDisposition = aDisposition; }
+ void GetDisposition(nsString& aDisposition) { aDisposition = mDisposition; }
+
+ // Get a Cocoa NSPrintInfo that is configured with our current settings.
+ // This follows Create semantics, so the caller is responsible to release
+ // the returned object when no longer required.
+ //
+ // Pass true for aWithScaling to have the print scaling factor included in
+ // the returned printInfo. Normally we pass false, as scaling is handled
+ // by Gecko and we don't want the Cocoa print system to impose scaling again
+ // on the output, but if we're retrieving the info in order to populate the
+ // system print UI, then we do want to know about it.
+ NSPrintInfo* CreateOrCopyPrintInfo(bool aWithScaling = false);
+
+ // Update our internal settings to reflect the properties of the given
+ // NSPrintInfo.
+ //
+ // If aAdoptPrintInfo is set, the given NSPrintInfo will be retained and
+ // returned by subsequent CreateOrCopyPrintInfo calls, which is required
+ // for custom settings from the OS print dialog to be passed through to
+ // print jobs. However, this means that subsequent changes to print settings
+ // via the generic nsPrintSettings methods will NOT be reflected in the
+ // resulting NSPrintInfo.
+ void SetFromPrintInfo(NSPrintInfo* aPrintInfo, bool aAdoptPrintInfo);
+
+ protected:
+ virtual ~nsPrintSettingsX() {
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ }
+ };
+
+ nsPrintSettingsX& operator=(const nsPrintSettingsX& rhs);
+
+ nsresult _Clone(nsIPrintSettings** _retval) override;
+ nsresult _Assign(nsIPrintSettings* aPS) override;
+
+ int GetCocoaUnit(int16_t aGeckoUnit);
+
+ double PaperSizeFromCocoaPoints(double aPointsValue) {
+ return aPointsValue * (mPaperSizeUnit == kPaperSizeInches ? 1.0 / 72.0 : 25.4 / 72.0);
+ }
+
+ double CocoaPointsFromPaperSize(double aSizeUnitValue) {
+ return aSizeUnitValue * (mPaperSizeUnit == kPaperSizeInches ? 72.0 : 72.0 / 25.4);
+ }
+
+ // Needed to correctly track the various job dispositions (spool, preview,
+ // save to file) that the user can choose via the system print dialog.
+ // Unfortunately it seems to be necessary to set both the Cocoa "job
+ // disposition" and the PrintManager "destination type" in order for all the
+ // various workflows such as "Save to Web Receipts" to work.
+ nsString mDisposition;
+ uint16_t mDestination;
+
+ // If the user has used the system print UI, we retain a reference to its
+ // printInfo because it may contain settings that we don't know how to handle
+ // and that will be lost if we round-trip through nsPrintSettings fields.
+ // We'll use this printInfo if asked to run a print job.
+ //
+ // This "wrapped" printInfo is NOT serialized or copied when printSettings
+ // objects are passed around; it is used only by the settings object to which
+ // it was originally passed.
+ NSPrintInfo* mSystemPrintInfo = nullptr;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintSettingsX, NS_PRINTSETTINGSX_IID)
+
+#endif // nsPrintSettingsX_h_
diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
new file mode 100644
index 0000000000..f7d6034f95
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -0,0 +1,362 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsPrintSettingsX.h"
+#include "nsObjCExceptions.h"
+
+#include "plbase64.h"
+
+#include "nsCocoaUtils.h"
+#include "nsXULAppAPI.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "nsPrinterCUPS.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsX, nsPrintSettings, nsPrintSettingsX)
+
+nsPrintSettingsX::nsPrintSettingsX() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mDestination = kPMDestinationInvalid;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
+ const PrintSettingsInitializer& aSettings) {
+ RefPtr<nsPrintSettings> settings = new nsPrintSettingsX();
+ settings->InitWithInitializer(aSettings);
+ settings->SetDefaultFileName();
+ return settings.forget();
+}
+
+nsPrintSettingsX& nsPrintSettingsX::operator=(const nsPrintSettingsX& rhs) {
+ if (this == &rhs) {
+ return *this;
+ }
+
+ nsPrintSettings::operator=(rhs);
+
+ mDestination = rhs.mDestination;
+ mDisposition = rhs.mDisposition;
+
+ // We don't copy mSystemPrintInfo here, so any copied printSettings will start out
+ // without a wrapped printInfo, just using our internal settings. The system
+ // printInfo is used *only* by the nsPrintSettingsX to which it was originally
+ // passed (when the user ran a system print UI dialog).
+
+ return *this;
+}
+
+nsresult nsPrintSettingsX::_Clone(nsIPrintSettings** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ auto newSettings = MakeRefPtr<nsPrintSettingsX>();
+ *newSettings = *this;
+ newSettings.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettingsX::_Assign(nsIPrintSettings* aPS) {
+ nsPrintSettingsX* printSettingsX = static_cast<nsPrintSettingsX*>(aPS);
+ if (!printSettingsX) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ *this = *printSettingsX;
+ return NS_OK;
+}
+
+struct KnownMonochromeSetting {
+ const NSString* mName;
+ const NSString* mValue;
+};
+
+#define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) \
+ { @key_, @value_ } \
+ ,
+static const KnownMonochromeSetting kKnownMonochromeSettings[] = {
+ CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)};
+#undef DECLARE_KNOWN_MONOCHROME_SETTING
+
+NSPrintInfo* nsPrintSettingsX::CreateOrCopyPrintInfo(bool aWithScaling) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // If we have a printInfo that came from the system print UI, use it so that
+ // any printer-specific settings we don't know about will still be used.
+ if (mSystemPrintInfo) {
+ NSPrintInfo* sysPrintInfo = [mSystemPrintInfo copy];
+ // Any required scaling will be done by Gecko, so we don't want it here.
+ [sysPrintInfo setScalingFactor:1.0f];
+ return sysPrintInfo;
+ }
+
+ // Note that the app shared `sharedPrintInfo` object is special! The system
+ // print dialog and print settings dialog update it with the values chosen
+ // by the user. Using that object here to initialize new nsPrintSettingsX
+ // objects could mask bugs in our code where we fail to save and/or restore
+ // print settings ourselves (e.g., bug 1636725). On other platforms we only
+ // initialize new nsPrintSettings objects from the settings that we save to
+ // prefs. Perhaps we should stop using sharedPrintInfo here for consistency?
+ NSPrintInfo* printInfo = [[NSPrintInfo sharedPrintInfo] copy];
+
+ NSSize paperSize;
+ if (GetSheetOrientation() == kPortraitOrientation) {
+ paperSize.width = CocoaPointsFromPaperSize(mPaperWidth);
+ paperSize.height = CocoaPointsFromPaperSize(mPaperHeight);
+ } else {
+ paperSize.width = CocoaPointsFromPaperSize(mPaperHeight);
+ paperSize.height = CocoaPointsFromPaperSize(mPaperWidth);
+ }
+ [printInfo setPaperSize:paperSize];
+
+ if (paperSize.width > paperSize.height) {
+ [printInfo setOrientation:NSPaperOrientationLandscape];
+ } else {
+ [printInfo setOrientation:NSPaperOrientationPortrait];
+ }
+
+ [printInfo setTopMargin:mUnwriteableMargin.top];
+ [printInfo setRightMargin:mUnwriteableMargin.right];
+ [printInfo setBottomMargin:mUnwriteableMargin.bottom];
+ [printInfo setLeftMargin:mUnwriteableMargin.left];
+
+ // If the printer name is the name of our pseudo print-to-PDF printer, the
+ // following `setPrinter` call will fail silently, since macOS doesn't know
+ // anything about it. That's OK, because mPrinter is our canonical source of
+ // truth.
+ // Actually, it seems Mac OS X 10.12 (the oldest version of Mac that we
+ // support) hangs if the printer name is not recognized. For now we explicitly
+ // check for our print-to-PDF printer, but that is not ideal since we should
+ // really localize the name of this printer at some point. Once we drop
+ // support for 10.12 we should remove this check.
+ if (!mPrinter.EqualsLiteral("Mozilla Save to PDF")) {
+ [printInfo setPrinter:[NSPrinter printerWithName:nsCocoaUtils::ToNSString(mPrinter)]];
+ }
+
+ // Scaling is handled by gecko, we do NOT want the cocoa printing system to add
+ // a second scaling on top of that. So we only set the true scaling factor here
+ // if the caller explicitly asked for it.
+ [printInfo setScalingFactor:CGFloat(aWithScaling ? mScaling : 1.0f)];
+
+ const bool allPages = mPageRanges.IsEmpty();
+
+ NSMutableDictionary* dict = [printInfo dictionary];
+ [dict setObject:[NSNumber numberWithInt:mNumCopies] forKey:NSPrintCopies];
+ [dict setObject:[NSNumber numberWithBool:allPages] forKey:NSPrintAllPages];
+
+ int32_t start = 1;
+ int32_t end = 1;
+ for (size_t i = 0; i < mPageRanges.Length(); i += 2) {
+ start = std::min(start, mPageRanges[i]);
+ end = std::max(end, mPageRanges[i + 1]);
+ }
+
+ [dict setObject:[NSNumber numberWithInt:start] forKey:NSPrintFirstPage];
+ [dict setObject:[NSNumber numberWithInt:end] forKey:NSPrintLastPage];
+
+ NSURL* jobSavingURL = nullptr;
+ if (!mToFileName.IsEmpty()) {
+ jobSavingURL = [NSURL fileURLWithPath:nsCocoaUtils::ToNSString(mToFileName)];
+ if (jobSavingURL) {
+ // Note: the PMPrintSettingsSetJobName call in nsPrintDialogServiceX::Show
+ // seems to mean that we get a sensible file name pre-populated in the
+ // dialog there, although our mToFileName is expected to be a full path,
+ // and it's less clear where the rest of the path (the directory to save
+ // to) in nsPrintDialogServiceX::Show comes from (perhaps from the use
+ // of `sharedPrintInfo` to initialize new nsPrintSettingsX objects).
+ [dict setObject:jobSavingURL forKey:NSPrintJobSavingURL];
+ }
+ }
+
+ if (mDisposition.IsEmpty()) {
+ // NOTE: It's unclear what to do for kOutputDestinationStream but this is
+ // only for the native print dialog where that can't happen.
+ if (mOutputDestination == kOutputDestinationFile) {
+ [printInfo setJobDisposition:NSPrintSaveJob];
+ } else {
+ [printInfo setJobDisposition:NSPrintSpoolJob];
+ }
+ } else {
+ [printInfo setJobDisposition:nsCocoaUtils::ToNSString(mDisposition)];
+ }
+
+ PMDuplexMode duplexSetting;
+ switch (mDuplex) {
+ default:
+ // This can't happen :) but if it does, we treat it as "none".
+ MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
+ case kDuplexNone:
+ duplexSetting = kPMDuplexNone;
+ break;
+ case kDuplexFlipOnLongEdge:
+ duplexSetting = kPMDuplexNoTumble;
+ break;
+ case kDuplexFlipOnShortEdge:
+ duplexSetting = kPMDuplexTumble;
+ break;
+ }
+
+ NSMutableDictionary* printSettings = [printInfo printSettings];
+ [printSettings setObject:[NSNumber numberWithUnsignedShort:duplexSetting]
+ forKey:@"com_apple_print_PrintSettings_PMDuplexing"];
+
+ if (mDestination != kPMDestinationInvalid) {
+ // Required to support PDF-workflow destinations such as Save to Web Receipts.
+ [printSettings setObject:[NSNumber numberWithUnsignedShort:mDestination]
+ forKey:@"com_apple_print_PrintSettings_PMDestinationType"];
+ if (jobSavingURL) {
+ [printSettings setObject:[jobSavingURL absoluteString]
+ forKey:@"com_apple_print_PrintSettings_PMOutputFilename"];
+ }
+ }
+
+ if (StaticPrefs::print_cups_monochrome_enabled() && !GetPrintInColor()) {
+ for (const auto& setting : kKnownMonochromeSettings) {
+ [printSettings setObject:setting.mValue forKey:setting.mName];
+ }
+ auto applySetting = [&](const nsACString& aKey, const nsACString& aValue) {
+ [printSettings setObject:nsCocoaUtils::ToNSString(aValue)
+ forKey:nsCocoaUtils::ToNSString(aKey)];
+ };
+ nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting);
+ }
+
+ return printInfo;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+void nsPrintSettingsX::SetFromPrintInfo(NSPrintInfo* aPrintInfo, bool aAdoptPrintInfo) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Set page-size/margins.
+ NSSize paperSize = [aPrintInfo paperSize];
+ const bool areSheetsOfPaperPortraitMode =
+ ([aPrintInfo orientation] == NSPaperOrientationPortrait);
+
+ // If our MacOS print settings say that we're producing portrait-mode sheets
+ // of paper, then our page format must also be portrait-mode; unless we've
+ // got a pages-per-sheet value with orthogonal pages/sheets, in which case
+ // it's reversed.
+ const bool arePagesPortraitMode = (areSheetsOfPaperPortraitMode != HasOrthogonalSheetsAndPages());
+
+ if (arePagesPortraitMode) {
+ mOrientation = nsIPrintSettings::kPortraitOrientation;
+ SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.width));
+ SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.height));
+ } else {
+ mOrientation = nsIPrintSettings::kLandscapeOrientation;
+ SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.height));
+ SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.width));
+ }
+
+ mUnwriteableMargin.top = [aPrintInfo topMargin];
+ mUnwriteableMargin.right = [aPrintInfo rightMargin];
+ mUnwriteableMargin.bottom = [aPrintInfo bottomMargin];
+ mUnwriteableMargin.left = [aPrintInfo leftMargin];
+
+ if (aAdoptPrintInfo) {
+ // Keep a reference to the printInfo; it may have settings that we don't know how to handle
+ // otherwise.
+ if (mSystemPrintInfo != aPrintInfo) {
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ }
+ mSystemPrintInfo = aPrintInfo;
+ [mSystemPrintInfo retain];
+ }
+ } else {
+ // Clear any stored printInfo.
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ mSystemPrintInfo = nullptr;
+ }
+ }
+
+ nsCocoaUtils::GetStringForNSString([[aPrintInfo printer] name], mPrinter);
+
+ // Only get the scaling value if shrink-to-fit is not selected:
+ bool isShrinkToFitChecked;
+ GetShrinkToFit(&isShrinkToFitChecked);
+ if (!isShrinkToFitChecked) {
+ // Limit scaling precision to whole percentage values.
+ mScaling = round(double([aPrintInfo scalingFactor]) * 100.0) / 100.0;
+ }
+
+ mOutputDestination = [&] {
+ if ([aPrintInfo jobDisposition] == NSPrintSaveJob) {
+ return kOutputDestinationFile;
+ }
+ return kOutputDestinationPrinter;
+ }();
+
+ NSDictionary* dict = [aPrintInfo dictionary];
+ const char* filePath = [[dict objectForKey:NSPrintJobSavingURL] fileSystemRepresentation];
+ if (filePath && *filePath) {
+ CopyUTF8toUTF16(Span(filePath, strlen(filePath)), mToFileName);
+ }
+
+ nsCocoaUtils::GetStringForNSString([aPrintInfo jobDisposition], mDisposition);
+
+ mNumCopies = [[dict objectForKey:NSPrintCopies] intValue];
+ mPageRanges.Clear();
+ if (![[dict objectForKey:NSPrintAllPages] boolValue]) {
+ mPageRanges.AppendElement([[dict objectForKey:NSPrintFirstPage] intValue]);
+ mPageRanges.AppendElement([[dict objectForKey:NSPrintLastPage] intValue]);
+ }
+
+ NSDictionary* printSettings = [aPrintInfo printSettings];
+ NSNumber* value = [printSettings objectForKey:@"com_apple_print_PrintSettings_PMDuplexing"];
+ if (value) {
+ PMDuplexMode duplexSetting = [value unsignedShortValue];
+ switch (duplexSetting) {
+ default:
+ // An unknown value is treated as None.
+ MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
+ case kPMDuplexNone:
+ mDuplex = kDuplexNone;
+ break;
+ case kPMDuplexNoTumble:
+ mDuplex = kDuplexFlipOnLongEdge;
+ break;
+ case kPMDuplexTumble:
+ mDuplex = kDuplexFlipOnShortEdge;
+ break;
+ }
+ } else {
+ // By default a printSettings dictionary doesn't initially contain the
+ // duplex key at all, so this is not an error; its absence just means no
+ // duplexing has been requested, so we return kDuplexNone.
+ mDuplex = kDuplexNone;
+ }
+
+ value = [printSettings objectForKey:@"com_apple_print_PrintSettings_PMDestinationType"];
+ if (value) {
+ mDestination = [value unsignedShortValue];
+ }
+
+ const bool color = [&] {
+ if (StaticPrefs::print_cups_monochrome_enabled()) {
+ for (const auto& setting : kKnownMonochromeSettings) {
+ NSString* value = [printSettings objectForKey:setting.mName];
+ if (!value) {
+ continue;
+ }
+ if ([setting.mValue isEqualToString:value]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }();
+
+ SetPrintInColor(color);
+
+ SetIsInitializedFromPrinter(true);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
diff --git a/widget/cocoa/nsSandboxViolationSink.h b/widget/cocoa/nsSandboxViolationSink.h
new file mode 100644
index 0000000000..d4b6e7ce07
--- /dev/null
+++ b/widget/cocoa/nsSandboxViolationSink.h
@@ -0,0 +1,36 @@
+/* -*- 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/. */
+
+#ifndef nsSandboxViolationSink_h_
+#define nsSandboxViolationSink_h_
+
+#include <stdint.h>
+
+// Class for tracking sandbox violations. Currently it just logs them to
+// stdout and the system console. In the future it may do more.
+
+// What makes this possible is the fact that Apple' sandboxd calls
+// notify_post("com.apple.sandbox.violation.*") whenever it's notified by the
+// Sandbox kernel extension of a sandbox violation. We register to receive
+// these notifications. But the notifications are empty, and are sent for
+// every violation in every process. So we need to do more to get only "our"
+// violations, and to find out what kind of violation they were. See the
+// implementation of nsSandboxViolationSink::ViolationHandler().
+
+#define SANDBOX_VIOLATION_QUEUE_NAME "org.mozilla.sandbox.violation.queue"
+#define SANDBOX_VIOLATION_NOTIFICATION_NAME "com.apple.sandbox.violation.*"
+
+class nsSandboxViolationSink {
+ public:
+ static void Start();
+ static void Stop();
+
+ private:
+ static void ViolationHandler();
+ static int mNotifyToken;
+ static uint64_t mLastMsgReceived;
+};
+
+#endif // nsSandboxViolationSink_h_
diff --git a/widget/cocoa/nsSandboxViolationSink.mm b/widget/cocoa/nsSandboxViolationSink.mm
new file mode 100644
index 0000000000..1399536d6e
--- /dev/null
+++ b/widget/cocoa/nsSandboxViolationSink.mm
@@ -0,0 +1,107 @@
+/* -*- 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/. */
+
+#include "nsSandboxViolationSink.h"
+
+#import <Foundation/NSObjCRuntime.h>
+
+#include <unistd.h>
+#include <time.h>
+#include <asl.h>
+#include <dispatch/dispatch.h>
+#include <notify.h>
+#include "mozilla/Preferences.h"
+#include "mozilla/Sprintf.h"
+
+int nsSandboxViolationSink::mNotifyToken = 0;
+uint64_t nsSandboxViolationSink::mLastMsgReceived = 0;
+
+void nsSandboxViolationSink::Start() {
+ if (mNotifyToken) {
+ return;
+ }
+ notify_register_dispatch(
+ SANDBOX_VIOLATION_NOTIFICATION_NAME, &mNotifyToken,
+ dispatch_queue_create(SANDBOX_VIOLATION_QUEUE_NAME, DISPATCH_QUEUE_SERIAL), ^(int token) {
+ ViolationHandler();
+ });
+}
+
+void nsSandboxViolationSink::Stop() {
+ if (!mNotifyToken) {
+ return;
+ }
+ notify_cancel(mNotifyToken);
+ mNotifyToken = 0;
+}
+
+// We need to query syslogd to find out what violations occurred, and whether
+// they were "ours". We can use the Apple System Log facility to do this.
+// Besides calling notify_post("com.apple.sandbox.violation.*"), Apple's
+// sandboxd also reports all sandbox violations (sent to it by the Sandbox
+// kernel extension) to syslogd, which stores them and makes them viewable
+// in the system console. This is the database we query.
+
+// ViolationHandler() is always called on its own secondary thread. This
+// makes it unlikely it will interfere with other browser activity.
+
+void nsSandboxViolationSink::ViolationHandler() {
+ aslmsg query = asl_new(ASL_TYPE_QUERY);
+
+ asl_set_query(query, ASL_KEY_FACILITY, "com.apple.sandbox", ASL_QUERY_OP_EQUAL);
+
+ // Only get reports that were generated very recently.
+ char query_time[30] = {0};
+ SprintfLiteral(query_time, "%li", time(NULL) - 2);
+ asl_set_query(query, ASL_KEY_TIME, query_time, ASL_QUERY_OP_NUMERIC | ASL_QUERY_OP_GREATER_EQUAL);
+
+ // This code is easier to test if we don't just track "our" violations,
+ // which are (normally) few and far between. For example (for the time
+ // being at least) four appleeventsd sandbox violations happen every time
+ // we start the browser in e10s mode. But it makes sense to default to
+ // only tracking "our" violations.
+ if (mozilla::Preferences::GetBool("security.sandbox.mac.track.violations.oursonly", true)) {
+ // This makes each of our processes log its own violations. It might
+ // be better to make the chrome process log all the other processes'
+ // violations.
+ char query_pid[20] = {0};
+ SprintfLiteral(query_pid, "%u", getpid());
+ asl_set_query(query, ASL_KEY_REF_PID, query_pid, ASL_QUERY_OP_EQUAL);
+ }
+
+ aslresponse response = asl_search(nullptr, query);
+
+ // Each time ViolationHandler() is called we grab as many messages as are
+ // available. Otherwise we might not get them all.
+ if (response) {
+ while (true) {
+ aslmsg hit = nullptr;
+ aslmsg found = nullptr;
+ const char* id_str;
+
+ while ((hit = aslresponse_next(response))) {
+ // Record the message id to avoid logging the same violation more
+ // than once.
+ id_str = asl_get(hit, ASL_KEY_MSG_ID);
+ uint64_t id_val = atoll(id_str);
+ if (id_val <= mLastMsgReceived) {
+ continue;
+ }
+ mLastMsgReceived = id_val;
+ found = hit;
+ break;
+ }
+ if (!found) {
+ break;
+ }
+
+ const char* pid_str = asl_get(found, ASL_KEY_REF_PID);
+ const char* message_str = asl_get(found, ASL_KEY_MSG);
+ NSLog(@"nsSandboxViolationSink::ViolationHandler(): id %s, pid %s, message %s", id_str,
+ pid_str, message_str);
+ }
+ aslresponse_free(response);
+ }
+}
diff --git a/widget/cocoa/nsSound.h b/widget/cocoa/nsSound.h
new file mode 100644
index 0000000000..cf3b02bd77
--- /dev/null
+++ b/widget/cocoa/nsSound.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsSound_h_
+#define nsSound_h_
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+class nsSound : public nsISound, public nsIStreamLoaderObserver {
+ public:
+ nsSound();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ protected:
+ virtual ~nsSound();
+};
+
+#endif // nsSound_h_
diff --git a/widget/cocoa/nsSound.mm b/widget/cocoa/nsSound.mm
new file mode 100644
index 0000000000..fb979f6f37
--- /dev/null
+++ b/widget/cocoa/nsSound.mm
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#include "nsSound.h"
+#include "nsContentUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsNetUtil.h"
+#include "nsCOMPtr.h"
+#include "nsIURL.h"
+#include "nsString.h"
+
+#import <Cocoa/Cocoa.h>
+
+NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver)
+
+nsSound::nsSound() {}
+
+nsSound::~nsSound() {}
+
+NS_IMETHODIMP
+nsSound::Beep() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSBeep();
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsSound::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* context, nsresult aStatus,
+ uint32_t dataLen, const uint8_t* data) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSData* value = [NSData dataWithBytes:data length:dataLen];
+
+ NSSound* sound = [[NSSound alloc] initWithData:value];
+
+ [sound play];
+
+ [sound autorelease];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsSound::Play(nsIURL* aURL) {
+ nsCOMPtr<nsIURI> uri(aURL);
+ nsCOMPtr<nsIStreamLoader> loader;
+ return NS_NewStreamLoader(getter_AddRefs(loader), uri,
+ this, // aObserver
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+}
+
+NS_IMETHODIMP
+nsSound::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsSound::PlayEventSound(uint32_t aEventId) {
+ // Mac doesn't have system sound settings for each user actions.
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsStandaloneNativeMenu.h b/widget/cocoa/nsStandaloneNativeMenu.h
new file mode 100644
index 0000000000..9d43e5e64d
--- /dev/null
+++ b/widget/cocoa/nsStandaloneNativeMenu.h
@@ -0,0 +1,27 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsStandaloneNativeMenu_h_
+#define nsStandaloneNativeMenu_h_
+
+#include "nsIStandaloneNativeMenu.h"
+#include "NativeMenuMac.h"
+
+class nsStandaloneNativeMenu : public nsIStandaloneNativeMenu {
+ public:
+ nsStandaloneNativeMenu();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTANDALONENATIVEMENU
+
+ RefPtr<mozilla::widget::NativeMenuMac> GetNativeMenu() { return mMenu; }
+
+ protected:
+ virtual ~nsStandaloneNativeMenu();
+
+ RefPtr<mozilla::widget::NativeMenuMac> mMenu;
+};
+
+#endif
diff --git a/widget/cocoa/nsStandaloneNativeMenu.mm b/widget/cocoa/nsStandaloneNativeMenu.mm
new file mode 100644
index 0000000000..d672f33314
--- /dev/null
+++ b/widget/cocoa/nsStandaloneNativeMenu.mm
@@ -0,0 +1,79 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsStandaloneNativeMenu.h"
+
+#include "mozilla/dom/Element.h"
+#include "NativeMenuMac.h"
+#include "nsISupports.h"
+#include "nsGkAtoms.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+
+NS_IMPL_ISUPPORTS(nsStandaloneNativeMenu, nsIStandaloneNativeMenu)
+
+nsStandaloneNativeMenu::nsStandaloneNativeMenu() = default;
+
+nsStandaloneNativeMenu::~nsStandaloneNativeMenu() = default;
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::Init(Element* aElement) {
+ NS_ASSERTION(mMenu == nullptr, "nsNativeMenu::Init - mMenu not null!");
+
+ NS_ENSURE_ARG(aElement);
+
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mMenu = new mozilla::widget::NativeMenuMac(aElement);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::MenuWillOpen(bool* aResult) {
+ NS_ASSERTION(mMenu != nullptr, "nsStandaloneNativeMenu::OnOpen - mMenu is null!");
+
+ mMenu->MenuWillOpen();
+
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
+ if (!mMenu) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (mMenu->ActivateNativeMenuItemAt(aIndexString)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ if (!mMenu) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mMenu->ForceUpdateNativeMenuAt(aIndexString);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::Dump() {
+ mMenu->Dump();
+
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsSystemStatusBarCocoa.h b/widget/cocoa/nsSystemStatusBarCocoa.h
new file mode 100644
index 0000000000..2af13e2805
--- /dev/null
+++ b/widget/cocoa/nsSystemStatusBarCocoa.h
@@ -0,0 +1,40 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsSystemStatusBarCocoa_h_
+#define nsSystemStatusBarCocoa_h_
+
+#include "mozilla/RefPtr.h"
+#include "nsISystemStatusBar.h"
+#include "nsClassHashtable.h"
+
+namespace mozilla::widget {
+class NativeMenuMac;
+}
+@class NSStatusItem;
+
+class nsSystemStatusBarCocoa : public nsISystemStatusBar {
+ public:
+ nsSystemStatusBarCocoa() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISYSTEMSTATUSBAR
+
+ protected:
+ virtual ~nsSystemStatusBarCocoa() {}
+
+ struct StatusItem {
+ explicit StatusItem(mozilla::widget::NativeMenuMac* aMenu);
+ ~StatusItem();
+
+ private:
+ RefPtr<mozilla::widget::NativeMenuMac> mMenu;
+ NSStatusItem* mStatusItem;
+ };
+
+ nsClassHashtable<nsISupportsHashKey, StatusItem> mItems;
+};
+
+#endif // nsSystemStatusBarCocoa_h_
diff --git a/widget/cocoa/nsSystemStatusBarCocoa.mm b/widget/cocoa/nsSystemStatusBarCocoa.mm
new file mode 100644
index 0000000000..6261a4b0c4
--- /dev/null
+++ b/widget/cocoa/nsSystemStatusBarCocoa.mm
@@ -0,0 +1,69 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsComponentManagerUtils.h"
+#include "nsSystemStatusBarCocoa.h"
+#include "NativeMenuMac.h"
+#include "nsObjCExceptions.h"
+#include "mozilla/dom/Element.h"
+
+using mozilla::dom::Element;
+using mozilla::widget::NativeMenuMac;
+
+NS_IMPL_ISUPPORTS(nsSystemStatusBarCocoa, nsISystemStatusBar)
+
+NS_IMETHODIMP
+nsSystemStatusBarCocoa::AddItem(Element* aElement) {
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<NativeMenuMac> menu = new NativeMenuMac(aElement);
+
+ nsCOMPtr<nsISupports> keyPtr = aElement;
+ mItems.InsertOrUpdate(keyPtr, mozilla::MakeUnique<StatusItem>(menu));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSystemStatusBarCocoa::RemoveItem(Element* aElement) {
+ mItems.Remove(aElement);
+ return NS_OK;
+}
+
+nsSystemStatusBarCocoa::StatusItem::StatusItem(NativeMenuMac* aMenu) : mMenu(aMenu) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsSystemStatusBarCocoa::StatusItem);
+
+ mStatusItem =
+ [[NSStatusBar.systemStatusBar statusItemWithLength:NSSquareStatusItemLength] retain];
+ mStatusItem.menu = mMenu->NativeNSMenu();
+ mStatusItem.highlightMode = YES;
+
+ // We want the status item to get its image from menu item that mMenu was
+ // initialized with. Icon loads are asynchronous, so we need to let the menu
+ // know about the item so that it can update its icon as soon as it has
+ // loaded.
+ mMenu->SetContainerStatusBarItem(mStatusItem);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsSystemStatusBarCocoa::StatusItem::~StatusItem() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenu->SetContainerStatusBarItem(nil);
+ [NSStatusBar.systemStatusBar removeStatusItem:mStatusItem];
+ [mStatusItem release];
+ mStatusItem = nil;
+
+ MOZ_COUNT_DTOR(nsSystemStatusBarCocoa::StatusItem);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
diff --git a/widget/cocoa/nsToolkit.h b/widget/cocoa/nsToolkit.h
new file mode 100644
index 0000000000..46b08d0ebe
--- /dev/null
+++ b/widget/cocoa/nsToolkit.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsToolkit_h_
+#define nsToolkit_h_
+
+#include "nscore.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#import <objc/Object.h>
+#import <IOKit/IOKitLib.h>
+
+class nsToolkit {
+ public:
+ nsToolkit();
+ virtual ~nsToolkit();
+
+ static nsToolkit* GetToolkit();
+
+ static void Shutdown() {
+ delete gToolkit;
+ gToolkit = nullptr;
+ }
+
+ static void PostSleepWakeNotification(const char* aNotification);
+
+ static nsresult SwizzleMethods(Class aClass, SEL orgMethod, SEL posedMethod,
+ bool classMethods = false);
+
+ void MonitorAllProcessMouseEvents();
+ void StopMonitoringAllProcessMouseEvents();
+
+ protected:
+ nsresult RegisterForSleepWakeNotifications();
+ void RemoveSleepWakeNotifications();
+
+ protected:
+ static nsToolkit* gToolkit;
+
+ CFRunLoopSourceRef mSleepWakeNotificationRLS;
+ io_object_t mPowerNotifier;
+
+ id mAllProcessMouseMonitor;
+};
+
+#endif // nsToolkit_h_
diff --git a/widget/cocoa/nsToolkit.mm b/widget/cocoa/nsToolkit.mm
new file mode 100644
index 0000000000..0180262067
--- /dev/null
+++ b/widget/cocoa/nsToolkit.mm
@@ -0,0 +1,252 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsToolkit.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <mach/mach_port.h>
+#include <mach/mach_interface.h>
+#include <mach/mach_init.h>
+
+extern "C" {
+#include <mach-o/getsect.h>
+}
+#include <unistd.h>
+#include <dlfcn.h>
+
+#import <Cocoa/Cocoa.h>
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <IOKit/IOMessage.h>
+
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+
+#include "nsGkAtoms.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsBaseWidget.h"
+
+#include "nsIObserverService.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+
+#include "NativeMenuSupport.h"
+
+using namespace mozilla;
+
+static io_connect_t gRootPort = MACH_PORT_NULL;
+
+nsToolkit* nsToolkit::gToolkit = nullptr;
+
+nsToolkit::nsToolkit()
+ : mSleepWakeNotificationRLS(nullptr), mPowerNotifier{0}, mAllProcessMouseMonitor(nil) {
+ MOZ_COUNT_CTOR(nsToolkit);
+ RegisterForSleepWakeNotifications();
+}
+
+nsToolkit::~nsToolkit() {
+ MOZ_COUNT_DTOR(nsToolkit);
+ RemoveSleepWakeNotifications();
+ StopMonitoringAllProcessMouseEvents();
+}
+
+void nsToolkit::PostSleepWakeNotification(const char* aNotification) {
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) observerService->NotifyObservers(nullptr, aNotification, nullptr);
+}
+
+// http://developer.apple.com/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/PowerMgmt/chapter_10_section_3.html
+static void ToolkitSleepWakeCallback(void* refCon, io_service_t service, natural_t messageType,
+ void* messageArgument) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ switch (messageType) {
+ case kIOMessageSystemWillSleep:
+ // System is going to sleep now.
+ nsToolkit::PostSleepWakeNotification(NS_WIDGET_SLEEP_OBSERVER_TOPIC);
+ ::IOAllowPowerChange(gRootPort, (long)messageArgument);
+ break;
+
+ case kIOMessageCanSystemSleep:
+ // In this case, the computer has been idle for several minutes
+ // and will sleep soon so you must either allow or cancel
+ // this notification. Important: if you don’t respond, there will
+ // be a 30-second timeout before the computer sleeps.
+ // In Mozilla's case, we always allow sleep.
+ ::IOAllowPowerChange(gRootPort, (long)messageArgument);
+ break;
+
+ case kIOMessageSystemHasPoweredOn:
+ // Handle wakeup.
+ nsToolkit::PostSleepWakeNotification(NS_WIDGET_WAKE_OBSERVER_TOPIC);
+ break;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsToolkit::RegisterForSleepWakeNotifications() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ IONotificationPortRef notifyPortRef;
+
+ NS_ASSERTION(!mSleepWakeNotificationRLS, "Already registered for sleep/wake");
+
+ gRootPort =
+ ::IORegisterForSystemPower(0, &notifyPortRef, ToolkitSleepWakeCallback, &mPowerNotifier);
+ if (gRootPort == MACH_PORT_NULL) {
+ NS_ERROR("IORegisterForSystemPower failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ mSleepWakeNotificationRLS = ::IONotificationPortGetRunLoopSource(notifyPortRef);
+ ::CFRunLoopAddSource(::CFRunLoopGetCurrent(), mSleepWakeNotificationRLS, kCFRunLoopDefaultMode);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsToolkit::RemoveSleepWakeNotifications() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mSleepWakeNotificationRLS) {
+ ::IODeregisterForSystemPower(&mPowerNotifier);
+ ::CFRunLoopRemoveSource(::CFRunLoopGetCurrent(), mSleepWakeNotificationRLS,
+ kCFRunLoopDefaultMode);
+
+ mSleepWakeNotificationRLS = nullptr;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Cocoa Firefox's use of custom context menus requires that we explicitly
+// handle mouse events from other processes that the OS handles
+// "automatically" for native context menus -- mouseMoved events so that
+// right-click context menus work properly when our browser doesn't have the
+// focus (bmo bug 368077), and mouseDown events so that our browser can
+// dismiss a context menu when a mouseDown happens in another process (bmo
+// bug 339945).
+void nsToolkit::MonitorAllProcessMouseEvents() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus()) {
+ // Don't do this if we are using native context menus.
+ return;
+ }
+
+ if (getenv("MOZ_NO_GLOBAL_MOUSE_MONITOR")) return;
+
+ if (mAllProcessMouseMonitor == nil) {
+ mAllProcessMouseMonitor = [NSEvent
+ addGlobalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown | NSEventMaskLeftMouseDown
+ handler:^(NSEvent* evt) {
+ if ([NSApp isActive]) {
+ return;
+ }
+
+ nsIRollupListener* rollupListener =
+ nsBaseWidget::GetActiveRollupListener();
+ if (!rollupListener) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> rollupWidget =
+ rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return;
+ }
+
+ NSWindow* ctxMenuWindow =
+ (NSWindow*)rollupWidget->GetNativeData(
+ NS_NATIVE_WINDOW);
+ if (!ctxMenuWindow) {
+ return;
+ }
+
+ // Don't roll up the rollup widget if our mouseDown happens
+ // over it (doing so would break the corresponding context
+ // menu).
+ NSPoint screenLocation = [NSEvent mouseLocation];
+ if (NSPointInRect(screenLocation, [ctxMenuWindow frame])) {
+ return;
+ }
+
+ rollupListener->Rollup({});
+ }];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsToolkit::StopMonitoringAllProcessMouseEvents() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mAllProcessMouseMonitor != nil) {
+ [NSEvent removeMonitor:mAllProcessMouseMonitor];
+ mAllProcessMouseMonitor = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Return the nsToolkit instance. If a toolkit does not yet exist, then one
+// will be created.
+// static
+nsToolkit* nsToolkit::GetToolkit() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!gToolkit) {
+ gToolkit = new nsToolkit();
+ }
+
+ return gToolkit;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+// An alternative to [NSObject poseAsClass:] that isn't deprecated on OS X
+// Leopard and is available to 64-bit binaries on Leopard and above. Based on
+// ideas and code from http://www.cocoadev.com/index.pl?MethodSwizzling.
+// Since the Method type becomes an opaque type as of Objective-C 2.0, we'll
+// have to switch to using accessor methods like method_exchangeImplementations()
+// when we build 64-bit binaries that use Objective-C 2.0 (on and for Leopard
+// and above).
+//
+// Be aware that, if aClass doesn't have an orgMethod selector but one of its
+// superclasses does, the method substitution will (in effect) take place in
+// that superclass (rather than in aClass itself). The substitution has
+// effect on the class where it takes place and all of that class's
+// subclasses. In order for method swizzling to work properly, posedMethod
+// needs to be unique in the class where the substitution takes place and all
+// of its subclasses.
+nsresult nsToolkit::SwizzleMethods(Class aClass, SEL orgMethod, SEL posedMethod,
+ bool classMethods) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Method original = nil;
+ Method posed = nil;
+
+ if (classMethods) {
+ original = class_getClassMethod(aClass, orgMethod);
+ posed = class_getClassMethod(aClass, posedMethod);
+ } else {
+ original = class_getInstanceMethod(aClass, orgMethod);
+ posed = class_getInstanceMethod(aClass, posedMethod);
+ }
+
+ if (!original || !posed) return NS_ERROR_FAILURE;
+
+ method_exchangeImplementations(original, posed);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsTouchBar.h b/widget/cocoa/nsTouchBar.h
new file mode 100644
index 0000000000..4432b05c39
--- /dev/null
+++ b/widget/cocoa/nsTouchBar.h
@@ -0,0 +1,136 @@
+/* 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/. */
+
+#ifndef nsTouchBar_h_
+#define nsTouchBar_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsITouchBarHelper.h"
+#include "nsTouchBarInput.h"
+#include "nsTouchBarNativeAPIDefines.h"
+
+const NSTouchBarItemIdentifier kTouchBarBaseIdentifier = @"com.mozilla.firefox.touchbar";
+
+/**
+ * Our TouchBar is its own delegate. This is adequate for our purposes,
+ * since the current implementation only defines Touch Bar buttons for the
+ * main Firefox window. If modals and other windows were to have custom
+ * Touch Bar views, each window would have to be a NSTouchBarDelegate so
+ * they could define their own custom sets of buttons.
+ */
+@interface nsTouchBar : NSTouchBar <NSTouchBarDelegate,
+ NSSharingServicePickerTouchBarItemDelegate,
+ NSSharingServiceDelegate> {
+ /**
+ * Link to the frontend API that determines which buttons appear
+ * in the Touch Bar
+ */
+ nsCOMPtr<nsITouchBarHelper> mTouchBarHelper;
+}
+
+/**
+ * Contains TouchBarInput representations of the inputs currently in
+ * the Touch Bar. Populated in `init` and updated by nsITouchBarUpdater.
+ */
+@property(strong) NSMutableDictionary<NSTouchBarItemIdentifier, TouchBarInput*>* mappedLayoutItems;
+
+/**
+ * Stores buttons displayed in a NSScrollView. They must be stored separately
+ * because they are untethered from the nsTouchBar. As such, they
+ * cannot be retrieved with [NSTouchBar itemForIdentifier].
+ */
+@property(strong)
+ NSMutableDictionary<NSTouchBarItemIdentifier, NSCustomTouchBarItem*>* scrollViewButtons;
+
+/**
+ * Returns an instance of nsTouchBar based on implementation details
+ * fetched from the frontend through nsTouchBarHelper.
+ */
+- (instancetype)init;
+
+/**
+ * If aInputs is not nil, a nsTouchBar containing the inputs specified is
+ * initialized. Otherwise, a nsTouchBar is initialized containing a default set
+ * of inputs.
+ */
+- (instancetype)initWithInputs:(NSMutableArray<TouchBarInput*>*)aInputs;
+
+- (void)dealloc;
+
+/**
+ * Creates a new NSTouchBarItem and adds it to the Touch Bar.
+ * Reads the passed identifier and creates the
+ * appropriate item type (eg. NSCustomTouchBarItem).
+ * Required as a member of NSTouchBarDelegate.
+ */
+- (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+
+/**
+ * Updates an input on the Touch Bar by redirecting to one of the specific
+ * TouchBarItem types updaters.
+ * Returns true if the input was successfully updated.
+ */
+- (bool)updateItem:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Checks to see if a given input exists within
+ * any of this Touch Bar's popovers and updates it if it exists.
+ */
+- (bool)maybeUpdatePopoverChild:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Checks to see if a given input exists within
+ * any of this Touch Bar's scroll views and updates it if it exists.
+ */
+- (bool)maybeUpdateScrollViewChild:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Replaces an item in the
+ * self.mappedLayoutItems dictionary.
+ */
+- (void)replaceMappedLayoutItem:(TouchBarInput*)aItem;
+
+/**
+ * Update or create various subclasses of TouchBarItem.
+ */
+- (void)updateButton:(NSCustomTouchBarItem*)aButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateMainButton:(NSCustomTouchBarItem*)aMainButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updatePopover:(NSPopoverTouchBarItem*)aPopoverItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateScrollView:(NSCustomTouchBarItem*)aScrollViewItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateLabel:(NSTextField*)aLabel withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (NSTouchBarItem*)makeShareScrubberForIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+
+/**
+ * If aShowing is true, aPopover is shown. Otherwise, it is hidden.
+ */
+- (void)showPopover:(TouchBarInput*)aPopover showing:(bool)aShowing;
+
+/**
+ * Redirects button actions to the appropriate handler.
+ */
+- (void)touchBarAction:(id)aSender;
+
+/**
+ * Helper function to initialize a new nsTouchBarInputIcon and load an icon.
+ */
+- (void)loadIconForInput:(TouchBarInput*)aInput forItem:(NSTouchBarItem*)aItem;
+
+- (NSArray*)itemsForSharingServicePickerTouchBarItem:
+ (NSSharingServicePickerTouchBarItem*)aPickerTouchBarItem;
+
+- (NSArray<NSSharingService*>*)sharingServicePicker:(NSSharingServicePicker*)aSharingServicePicker
+ sharingServicesForItems:(NSArray*)aItems
+ proposedSharingServices:(NSArray<NSSharingService*>*)aProposedServices;
+
+- (void)releaseJSObjects;
+
+@end // nsTouchBar
+
+#endif // nsTouchBar_h_
diff --git a/widget/cocoa/nsTouchBar.mm b/widget/cocoa/nsTouchBar.mm
new file mode 100644
index 0000000000..f6a7042381
--- /dev/null
+++ b/widget/cocoa/nsTouchBar.mm
@@ -0,0 +1,605 @@
+/* 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/. */
+
+#include "nsTouchBar.h"
+
+#include <objc/runtime.h>
+
+#include "mozilla/MacStringHelpers.h"
+#include "mozilla/dom/Document.h"
+#include "nsArrayUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIArray.h"
+#include "nsTouchBarInputIcon.h"
+#include "nsWidgetsCID.h"
+
+@implementation nsTouchBar
+
+// Used to tie action strings to buttons.
+static char sIdentifierAssociationKey;
+
+// The default space between inputs, used where layout is not automatic.
+static const uint32_t kInputSpacing = 8;
+// The width of buttons in Apple's Share ScrollView. We use this in our
+// ScrollViews to give them a native appearance.
+static const uint32_t kScrollViewButtonWidth = 144;
+static const uint32_t kInputIconSize = 16;
+
+// The system default width for Touch Bar inputs is 128px. This is double.
+#define MAIN_BUTTON_WIDTH 256
+
+#pragma mark - NSTouchBarDelegate
+
+- (instancetype)init {
+ return [self initWithInputs:nil];
+}
+
+- (instancetype)initWithInputs:(NSMutableArray<TouchBarInput*>*)aInputs {
+ if ((self = [super init])) {
+ mTouchBarHelper = do_GetService(NS_TOUCHBARHELPER_CID);
+ if (!mTouchBarHelper) {
+ NS_ERROR("Unable to create Touch Bar Helper.");
+ return nil;
+ }
+
+ self.delegate = self;
+ self.mappedLayoutItems = [NSMutableDictionary dictionary];
+ self.customizationAllowedItemIdentifiers = @[];
+
+ if (!aInputs) {
+ // This customization identifier is how users' custom layouts are saved by macOS.
+ // If this changes, all users' layouts would be reset to the default layout.
+ self.customizationIdentifier =
+ [kTouchBarBaseIdentifier stringByAppendingPathExtension:@"defaultbar"];
+ nsCOMPtr<nsIArray> allItems;
+
+ nsresult rv = mTouchBarHelper->GetAllItems(getter_AddRefs(allItems));
+ if (NS_FAILED(rv) || !allItems) {
+ return nil;
+ }
+
+ uint32_t itemCount = 0;
+ allItems->GetLength(&itemCount);
+ // This is copied to self.customizationAllowedItemIdentifiers.
+ // Required since [self.mappedItems allKeys] does not preserve order.
+ // One slot is added for the spacer item.
+ NSMutableArray* orderedIdentifiers = [NSMutableArray arrayWithCapacity:itemCount + 1];
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> input = do_QueryElementAt(allItems, i);
+ if (!input) {
+ continue;
+ }
+
+ TouchBarInput* convertedInput;
+ NSTouchBarItemIdentifier newInputIdentifier =
+ [TouchBarInput nativeIdentifierWithXPCOM:input];
+ if (!newInputIdentifier) {
+ continue;
+ }
+
+ // If there is already an input in mappedLayoutItems with this identifier,
+ // that means updateItem fired before this initialization. The input
+ // cached by updateItem is more current, so we should use that one.
+ if (self.mappedLayoutItems[newInputIdentifier]) {
+ convertedInput = self.mappedLayoutItems[newInputIdentifier];
+ } else {
+ convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
+ // Add new input to dictionary for lookup of properties in delegate.
+ self.mappedLayoutItems[[convertedInput nativeIdentifier]] = convertedInput;
+ }
+
+ orderedIdentifiers[i] = [convertedInput nativeIdentifier];
+ }
+ [orderedIdentifiers addObject:@"NSTouchBarItemIdentifierFlexibleSpace"];
+ self.customizationAllowedItemIdentifiers = [orderedIdentifiers copy];
+
+ NSArray* defaultItemIdentifiers = @[
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"back"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"forward"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"reload"],
+ [TouchBarInput nativeIdentifierWithType:@"mainButton" withKey:@"open-location"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"new-tab"],
+ [TouchBarInput shareScrubberIdentifier], [TouchBarInput searchPopoverIdentifier]
+ ];
+ self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
+ } else {
+ NSMutableArray* defaultItemIdentifiers = [NSMutableArray arrayWithCapacity:[aInputs count]];
+ for (TouchBarInput* input in aInputs) {
+ self.mappedLayoutItems[[input nativeIdentifier]] = input;
+ [defaultItemIdentifiers addObject:[input nativeIdentifier]];
+ }
+ self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
+ }
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ NSTouchBarItem* item = [self itemForIdentifier:identifier];
+ if (!item) {
+ continue;
+ }
+ if ([item isKindOfClass:[NSPopoverTouchBarItem class]]) {
+ [(NSPopoverTouchBarItem*)item setCollapsedRepresentationImage:nil];
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar] release];
+ } else if ([[item view] isKindOfClass:[NSScrollView class]]) {
+ [[(NSScrollView*)[item view] documentView] release];
+ [(NSScrollView*)[item view] release];
+ }
+
+ [item release];
+ }
+
+ [self.defaultItemIdentifiers release];
+ [self.customizationAllowedItemIdentifiers release];
+ [self.scrollViewButtons removeAllObjects];
+ [self.scrollViewButtons release];
+ [self.mappedLayoutItems removeAllObjects];
+ [self.mappedLayoutItems release];
+ [super dealloc];
+}
+
+- (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!mTouchBarHelper) {
+ return nil;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return nil;
+ }
+
+ if ([input baseType] == TouchBarInputBaseType::kScrubber) {
+ // We check the identifier rather than the baseType here as a special case.
+ if (![aIdentifier isEqualToString:[TouchBarInput shareScrubberIdentifier]]) {
+ // We're only supporting the Share scrubber for now.
+ return nil;
+ }
+ return [self makeShareScrubberForIdentifier:aIdentifier];
+ }
+
+ if ([input baseType] == TouchBarInputBaseType::kPopover) {
+ NSPopoverTouchBarItem* newPopoverItem =
+ [[NSPopoverTouchBarItem alloc] initWithIdentifier:aIdentifier];
+ [newPopoverItem setCustomizationLabel:[input title]];
+ // We initialize popoverTouchBar here because we only allow setting this
+ // property on popover creation. Updating popoverTouchBar for every update
+ // of the popover item would be very expensive.
+ newPopoverItem.popoverTouchBar = [[nsTouchBar alloc] initWithInputs:[input children]];
+ [self updatePopover:newPopoverItem withIdentifier:[input nativeIdentifier]];
+ return newPopoverItem;
+ }
+
+ // Our new item, which will be initialized depending on aIdentifier.
+ NSCustomTouchBarItem* newItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:aIdentifier];
+ [newItem setCustomizationLabel:[input title]];
+
+ if ([input baseType] == TouchBarInputBaseType::kScrollView) {
+ [self updateScrollView:newItem withIdentifier:[input nativeIdentifier]];
+ return newItem;
+ } else if ([input baseType] == TouchBarInputBaseType::kLabel) {
+ NSTextField* label = [NSTextField labelWithString:@""];
+ [self updateLabel:label withIdentifier:[input nativeIdentifier]];
+ newItem.view = label;
+ return newItem;
+ }
+
+ // The cases of a button or main button require the same setup.
+ NSButton* button = [NSButton buttonWithTitle:@"" target:self action:@selector(touchBarAction:)];
+ newItem.view = button;
+
+ if ([input baseType] == TouchBarInputBaseType::kButton &&
+ ![[input type] hasPrefix:@"scrollView"]) {
+ [self updateButton:newItem withIdentifier:[input nativeIdentifier]];
+ } else if ([input baseType] == TouchBarInputBaseType::kMainButton) {
+ [self updateMainButton:newItem withIdentifier:[input nativeIdentifier]];
+ }
+ return newItem;
+}
+
+- (bool)updateItem:(TouchBarInput*)aInput {
+ if (!mTouchBarHelper) {
+ return false;
+ }
+
+ NSTouchBarItem* item = [self itemForIdentifier:[aInput nativeIdentifier]];
+
+ // If we can't immediately find item, there are three possibilities:
+ // * It is a button in a ScrollView, or
+ // * It is contained within a popover, or
+ // * It simply does not exist.
+ // We check for each possibility here.
+ if (!self.mappedLayoutItems[[aInput nativeIdentifier]]) {
+ if ([self maybeUpdateScrollViewChild:aInput]) {
+ return true;
+ }
+ if ([self maybeUpdatePopoverChild:aInput]) {
+ return true;
+ }
+ return false;
+ }
+
+ // Update our canonical copy of the input.
+ [self replaceMappedLayoutItem:aInput];
+
+ if ([aInput baseType] == TouchBarInputBaseType::kButton) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateButton:(NSCustomTouchBarItem*)item withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kMainButton) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateMainButton:(NSCustomTouchBarItem*)item withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kScrollView) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateScrollView:(NSCustomTouchBarItem*)item withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kPopover) {
+ [(NSPopoverTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updatePopover:(NSPopoverTouchBarItem*)item withIdentifier:[aInput nativeIdentifier]];
+ for (TouchBarInput* child in [aInput children]) {
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar] updateItem:child];
+ }
+ } else if ([aInput baseType] == TouchBarInputBaseType::kLabel) {
+ [self updateLabel:(NSTextField*)item.view withIdentifier:[aInput nativeIdentifier]];
+ }
+
+ return true;
+}
+
+- (bool)maybeUpdatePopoverChild:(TouchBarInput*)aInput {
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* potentialPopover = self.mappedLayoutItems[identifier];
+ if ([potentialPopover baseType] != TouchBarInputBaseType::kPopover) {
+ continue;
+ }
+ NSTouchBarItem* popover = [self itemForIdentifier:[potentialPopover nativeIdentifier]];
+ if (popover) {
+ if ([(nsTouchBar*)[(NSPopoverTouchBarItem*)popover popoverTouchBar] updateItem:aInput]) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+- (bool)maybeUpdateScrollViewChild:(TouchBarInput*)aInput {
+ NSCustomTouchBarItem* scrollViewButton = self.scrollViewButtons[[aInput nativeIdentifier]];
+ if (scrollViewButton) {
+ // ScrollView buttons are similar to mainButtons except for their width.
+ [self updateMainButton:scrollViewButton withIdentifier:[aInput nativeIdentifier]];
+ NSButton* button = (NSButton*)scrollViewButton.view;
+ uint32_t buttonSize = MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
+ kScrollViewButtonWidth);
+ [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize].active = YES;
+ }
+ // Updating the TouchBarInput* in the ScrollView's mChildren array.
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* potentialScrollView = self.mappedLayoutItems[identifier];
+ if ([potentialScrollView baseType] != TouchBarInputBaseType::kScrollView) {
+ continue;
+ }
+ for (uint32_t i = 0; i < [[potentialScrollView children] count]; ++i) {
+ TouchBarInput* child = [potentialScrollView children][i];
+ if (![[child nativeIdentifier] isEqualToString:[aInput nativeIdentifier]]) {
+ continue;
+ }
+ [[potentialScrollView children] replaceObjectAtIndex:i withObject:aInput];
+ [child release];
+ return true;
+ }
+ }
+ return false;
+}
+
+- (void)replaceMappedLayoutItem:(TouchBarInput*)aItem {
+ [self.mappedLayoutItems[[aItem nativeIdentifier]] release];
+ self.mappedLayoutItems[[aItem nativeIdentifier]] = aItem;
+}
+
+- (void)updateButton:(NSCustomTouchBarItem*)aButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aButton || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ NSButton* button = (NSButton*)[aButton view];
+ button.title = [input title];
+ if ([input imageURI]) {
+ [button setImagePosition:NSImageOnly];
+ [self loadIconForInput:input forItem:aButton];
+ // Because we are hiding the title, NSAccessibility also does not get it.
+ // Therefore, set an accessibility label as alternative text for image-only buttons.
+ [button setAccessibilityLabel:[input title]];
+ }
+
+ [button setEnabled:![input isDisabled]];
+ if ([input color]) {
+ button.bezelColor = [input color];
+ }
+
+ objc_setAssociatedObject(button, &sIdentifierAssociationKey, aIdentifier,
+ OBJC_ASSOCIATION_RETAIN);
+}
+
+- (void)updateMainButton:(NSCustomTouchBarItem*)aMainButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aMainButton || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ [self updateButton:aMainButton withIdentifier:aIdentifier];
+ NSButton* button = (NSButton*)[aMainButton view];
+
+ // If empty, string is still being localized. Display a blank input instead.
+ if ([[input title] isEqualToString:@""]) {
+ [button setImagePosition:NSNoImage];
+ } else {
+ [button setImagePosition:NSImageLeft];
+ }
+ button.imageHugsTitle = YES;
+ [button.widthAnchor constraintGreaterThanOrEqualToConstant:MAIN_BUTTON_WIDTH].active = YES;
+ [button setContentHuggingPriority:1.0 forOrientation:NSLayoutConstraintOrientationHorizontal];
+}
+
+- (void)updatePopover:(NSPopoverTouchBarItem*)aPopoverItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aPopoverItem || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ aPopoverItem.showsCloseButton = YES;
+ if ([input imageURI]) {
+ [self loadIconForInput:input forItem:aPopoverItem];
+ } else if ([input title]) {
+ aPopoverItem.collapsedRepresentationLabel = [input title];
+ }
+
+ // Special handling to show/hide the search popover if the Urlbar is focused.
+ if ([[input nativeIdentifier] isEqualToString:[TouchBarInput searchPopoverIdentifier]]) {
+ // We can reach this code during window shutdown. We only want to toggle
+ // showPopover if we are in a normal running state.
+ if (!mTouchBarHelper) {
+ return;
+ }
+ bool urlbarIsFocused = false;
+ mTouchBarHelper->GetIsUrlbarFocused(&urlbarIsFocused);
+ if (urlbarIsFocused) {
+ [aPopoverItem showPopover:self];
+ }
+ }
+}
+
+- (void)updateScrollView:(NSCustomTouchBarItem*)aScrollViewItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aScrollViewItem || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input || ![input children]) {
+ return;
+ }
+
+ NSMutableDictionary* constraintViews = [NSMutableDictionary dictionary];
+ NSView* documentView = [[NSView alloc] initWithFrame:NSZeroRect];
+ NSString* layoutFormat = @"H:|-8-";
+ NSSize size = NSMakeSize(kInputSpacing, 30);
+ // Layout strings allow only alphanumeric characters. We will use this
+ // NSCharacterSet to strip illegal characters.
+ NSCharacterSet* charactersToRemove = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
+
+ for (TouchBarInput* childInput in [input children]) {
+ if ([childInput baseType] != TouchBarInputBaseType::kButton) {
+ continue;
+ }
+ [self replaceMappedLayoutItem:childInput];
+ NSCustomTouchBarItem* newItem =
+ [[NSCustomTouchBarItem alloc] initWithIdentifier:[childInput nativeIdentifier]];
+ NSButton* button = [NSButton buttonWithTitle:[childInput title]
+ target:self
+ action:@selector(touchBarAction:)];
+ newItem.view = button;
+ // ScrollView buttons are similar to mainButtons except for their width.
+ [self updateMainButton:newItem withIdentifier:[childInput nativeIdentifier]];
+ uint32_t buttonSize = MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
+ kScrollViewButtonWidth);
+ [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize].active = YES;
+
+ NSCustomTouchBarItem* tempItem = self.scrollViewButtons[[childInput nativeIdentifier]];
+ self.scrollViewButtons[[childInput nativeIdentifier]] = newItem;
+ [tempItem release];
+
+ button.translatesAutoresizingMaskIntoConstraints = NO;
+ [documentView addSubview:button];
+ NSString* layoutKey = [[[childInput nativeIdentifier]
+ componentsSeparatedByCharactersInSet:charactersToRemove] componentsJoinedByString:@""];
+
+ // Iteratively create our layout string.
+ layoutFormat =
+ [layoutFormat stringByAppendingString:[NSString stringWithFormat:@"[%@]-8-", layoutKey]];
+ [constraintViews setObject:button forKey:layoutKey];
+ size.width += kInputSpacing + buttonSize;
+ }
+ layoutFormat = [layoutFormat stringByAppendingString:[NSString stringWithFormat:@"|"]];
+ NSArray* hConstraints =
+ [NSLayoutConstraint constraintsWithVisualFormat:layoutFormat
+ options:NSLayoutFormatAlignAllCenterY
+ metrics:nil
+ views:constraintViews];
+ NSScrollView* scrollView =
+ [[NSScrollView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
+ [documentView setFrame:NSMakeRect(0, 0, size.width, size.height)];
+ [NSLayoutConstraint activateConstraints:hConstraints];
+ scrollView.documentView = documentView;
+
+ aScrollViewItem.view = scrollView;
+}
+
+- (void)updateLabel:(NSTextField*)aLabel withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aLabel || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input || ![input title]) {
+ return;
+ }
+ [aLabel setStringValue:[input title]];
+}
+
+- (NSTouchBarItem*)makeShareScrubberForIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ // System-default share menu
+ NSSharingServicePickerTouchBarItem* servicesItem =
+ [[NSSharingServicePickerTouchBarItem alloc] initWithIdentifier:aIdentifier];
+
+ // buttonImage needs to be set to nil while we wait for our icon to load.
+ // Otherwise, the default Apple share icon is automatically loaded.
+ servicesItem.buttonImage = nil;
+
+ [self loadIconForInput:input forItem:servicesItem];
+
+ servicesItem.delegate = self;
+ return servicesItem;
+}
+
+- (void)showPopover:(TouchBarInput*)aPopover showing:(bool)aShowing {
+ if (!aPopover) {
+ return;
+ }
+ NSPopoverTouchBarItem* popoverItem =
+ (NSPopoverTouchBarItem*)[self itemForIdentifier:[aPopover nativeIdentifier]];
+ if (!popoverItem) {
+ return;
+ }
+ if (aShowing) {
+ [popoverItem showPopover:self];
+ } else {
+ [popoverItem dismissPopover:self];
+ }
+}
+
+- (void)touchBarAction:(id)aSender {
+ NSTouchBarItemIdentifier identifier =
+ objc_getAssociatedObject(aSender, &sIdentifierAssociationKey);
+ if (!identifier || [identifier isEqualToString:@""]) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[identifier];
+ if (!input) {
+ return;
+ }
+
+ nsCOMPtr<nsITouchBarInputCallback> callback = [input callback];
+ if (!callback) {
+ NSLog(@"Touch Bar action attempted with no valid callback! Identifier: %@",
+ [input nativeIdentifier]);
+ return;
+ }
+ callback->OnCommand();
+}
+
+- (void)loadIconForInput:(TouchBarInput*)aInput forItem:(NSTouchBarItem*)aItem {
+ if (!aInput || ![aInput imageURI] || !aItem || !mTouchBarHelper) {
+ return;
+ }
+
+ RefPtr<nsTouchBarInputIcon> icon = [aInput icon];
+
+ if (!icon) {
+ RefPtr<Document> document;
+ nsresult rv = mTouchBarHelper->GetDocument(getter_AddRefs(document));
+ if (NS_FAILED(rv) || !document) {
+ return;
+ }
+ icon = new nsTouchBarInputIcon(document, aInput, aItem);
+ [aInput setIcon:icon];
+ }
+ icon->SetupIcon([aInput imageURI]);
+}
+
+- (void)releaseJSObjects {
+ mTouchBarHelper = nil;
+
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* input = self.mappedLayoutItems[identifier];
+ if (!input) {
+ continue;
+ }
+
+ // Childless popovers contain the default Touch Bar as its popoverTouchBar.
+ // We check for [input children] since the default Touch Bar contains a
+ // popover (search-popover), so this would infinitely loop if there was no check.
+ if ([input baseType] == TouchBarInputBaseType::kPopover && [input children]) {
+ NSTouchBarItem* item = [self itemForIdentifier:identifier];
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar] releaseJSObjects];
+ }
+
+ [input releaseJSObjects];
+ }
+}
+
+#pragma mark - NSSharingServicePickerTouchBarItemDelegate
+
+- (NSArray*)itemsForSharingServicePickerTouchBarItem:
+ (NSSharingServicePickerTouchBarItem*)aPickerTouchBarItem {
+ NSURL* urlToShare = nil;
+ NSString* titleToShare = @"";
+ nsAutoString url;
+ nsAutoString title;
+ if (mTouchBarHelper) {
+ nsresult rv = mTouchBarHelper->GetActiveUrl(url);
+ if (!NS_FAILED(rv)) {
+ urlToShare = [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
+ // NSURL URLWithString returns nil if the URL is invalid. At this point,
+ // it is too late to simply shut down the share menu, so we default to
+ // about:blank if the share button is clicked when the URL is invalid.
+ if (urlToShare == nil) {
+ urlToShare = [NSURL URLWithString:@"about:blank"];
+ }
+ }
+
+ rv = mTouchBarHelper->GetActiveTitle(title);
+ if (!NS_FAILED(rv)) {
+ titleToShare = nsCocoaUtils::ToNSString(title);
+ }
+ }
+
+ return @[ urlToShare, titleToShare ];
+}
+
+- (NSArray<NSSharingService*>*)sharingServicePicker:(NSSharingServicePicker*)aSharingServicePicker
+ sharingServicesForItems:(NSArray*)aItems
+ proposedSharingServices:(NSArray<NSSharingService*>*)aProposedServices {
+ // redundant services
+ NSArray* excludedServices = @[
+ @"com.apple.share.System.add-to-safari-reading-list",
+ ];
+
+ NSArray* sharingServices = [aProposedServices
+ filteredArrayUsingPredicate:[NSPredicate
+ predicateWithFormat:@"NOT (name IN %@)", excludedServices]];
+
+ return sharingServices;
+}
+
+@end
diff --git a/widget/cocoa/nsTouchBarInput.h b/widget/cocoa/nsTouchBarInput.h
new file mode 100644
index 0000000000..4853660d79
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInput.h
@@ -0,0 +1,90 @@
+/* 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/. */
+
+#ifndef nsTouchBarInput_h_
+#define nsTouchBarInput_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsITouchBarInput.h"
+#include "nsTouchBarNativeAPIDefines.h"
+#include "nsCOMPtr.h"
+
+using namespace mozilla::dom;
+
+enum class TouchBarInputBaseType : uint8_t {
+ kButton,
+ kLabel,
+ kMainButton,
+ kPopover,
+ kScrollView,
+ kScrubber
+};
+
+class nsTouchBarInputIcon;
+
+/**
+ * NSObject representation of nsITouchBarInput.
+ */
+@interface TouchBarInput : NSObject {
+ nsCOMPtr<nsIURI> mImageURI;
+ RefPtr<nsTouchBarInputIcon> mIcon;
+ TouchBarInputBaseType mBaseType;
+ NSString* mType;
+ nsCOMPtr<nsITouchBarInputCallback> mCallback;
+ NSMutableArray<TouchBarInput*>* mChildren;
+}
+
+@property(strong) NSString* key;
+@property(strong) NSString* type;
+@property(strong) NSString* title;
+@property(strong) NSColor* color;
+@property(nonatomic, getter=isDisabled) BOOL disabled;
+
+- (nsCOMPtr<nsIURI>)imageURI;
+- (RefPtr<nsTouchBarInputIcon>)icon;
+- (TouchBarInputBaseType)baseType;
+- (NSTouchBarItemIdentifier)nativeIdentifier;
+- (nsCOMPtr<nsITouchBarInputCallback>)callback;
+- (NSMutableArray<TouchBarInput*>*)children;
+- (void)setImageURI:(nsCOMPtr<nsIURI>)aImageURI;
+- (void)setIcon:(RefPtr<nsTouchBarInputIcon>)aIcon;
+- (void)setCallback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback;
+- (void)setChildren:(NSMutableArray<TouchBarInput*>*)aChildren;
+
+- (id)initWithKey:(NSString*)aKey
+ title:(NSString*)aTitle
+ imageURI:(nsCOMPtr<nsIURI>)aImageURI
+ type:(NSString*)aType
+ callback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback
+ color:(uint32_t)aColor
+ disabled:(BOOL)aDisabled
+ children:(nsCOMPtr<nsIArray>)aChildren;
+
+- (TouchBarInput*)initWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput;
+
+- (void)releaseJSObjects;
+
+- (void)dealloc;
+
+/**
+ * We make these helper methods static so that other classes can query a
+ * TouchBarInput's nativeIdentifier (e.g. nsTouchBarUpdater looking up a
+ * popover in mappedLayoutItems).
+ */
++ (NSTouchBarItemIdentifier)nativeIdentifierWithType:(NSString*)aType withKey:(NSString*)aKey;
++ (NSTouchBarItemIdentifier)nativeIdentifierWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput;
+
+// Non-JS scrubber implemention for the Share Scrubber,
+// since it is defined by an Apple API.
++ (NSTouchBarItemIdentifier)shareScrubberIdentifier;
+
+// The search popover needs to show/hide depending on if the Urlbar is focused
+// when it is created. We keep track of its identifier to accommodate this
+// special handling.
++ (NSTouchBarItemIdentifier)searchPopoverIdentifier;
+
+@end
+
+#endif // nsTouchBarInput_h_
diff --git a/widget/cocoa/nsTouchBarInput.mm b/widget/cocoa/nsTouchBarInput.mm
new file mode 100644
index 0000000000..dc50c64e1b
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInput.mm
@@ -0,0 +1,245 @@
+/* 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/. */
+
+#include "nsTouchBarInput.h"
+
+#include "mozilla/MacStringHelpers.h"
+#include "nsArrayUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsTouchBar.h"
+#include "nsTouchBarInputIcon.h"
+
+@implementation TouchBarInput
+
+- (nsCOMPtr<nsIURI>)imageURI {
+ return mImageURI;
+}
+
+- (void)setImageURI:(nsCOMPtr<nsIURI>)aImageURI {
+ mImageURI = aImageURI;
+}
+
+- (RefPtr<nsTouchBarInputIcon>)icon {
+ return mIcon;
+}
+
+- (void)setIcon:(RefPtr<nsTouchBarInputIcon>)aIcon {
+ mIcon = aIcon;
+}
+
+- (TouchBarInputBaseType)baseType {
+ return mBaseType;
+}
+
+- (NSString*)type {
+ return mType;
+}
+
+- (void)setType:(NSString*)aType {
+ [aType retain];
+ [mType release];
+ if ([aType hasSuffix:@"button"]) {
+ mBaseType = TouchBarInputBaseType::kButton;
+ } else if ([aType hasSuffix:@"label"]) {
+ mBaseType = TouchBarInputBaseType::kLabel;
+ } else if ([aType hasSuffix:@"mainButton"]) {
+ mBaseType = TouchBarInputBaseType::kMainButton;
+ } else if ([aType hasSuffix:@"popover"]) {
+ mBaseType = TouchBarInputBaseType::kPopover;
+ } else if ([aType hasSuffix:@"scrollView"]) {
+ mBaseType = TouchBarInputBaseType::kScrollView;
+ } else if ([aType hasSuffix:@"scrubber"]) {
+ mBaseType = TouchBarInputBaseType::kScrubber;
+ }
+ mType = aType;
+}
+
+- (NSTouchBarItemIdentifier)nativeIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:mType withKey:self.key];
+}
+
+- (nsCOMPtr<nsITouchBarInputCallback>)callback {
+ return mCallback;
+}
+
+- (void)setCallback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback {
+ mCallback = aCallback;
+}
+
+- (NSMutableArray<TouchBarInput*>*)children {
+ return mChildren;
+}
+
+- (void)setChildren:(NSMutableArray<TouchBarInput*>*)aChildren {
+ [aChildren retain];
+ for (TouchBarInput* child in mChildren) {
+ [child releaseJSObjects];
+ }
+ [mChildren removeAllObjects];
+ [mChildren release];
+ mChildren = aChildren;
+}
+
+- (id)initWithKey:(NSString*)aKey
+ title:(NSString*)aTitle
+ imageURI:(nsCOMPtr<nsIURI>)aImageURI
+ type:(NSString*)aType
+ callback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback
+ color:(uint32_t)aColor
+ disabled:(BOOL)aDisabled
+ children:(nsCOMPtr<nsIArray>)aChildren {
+ if (self = [super init]) {
+ mType = nil;
+
+ self.key = aKey;
+ self.title = aTitle;
+ self.type = aType;
+ self.disabled = aDisabled;
+ [self setImageURI:aImageURI];
+ [self setCallback:aCallback];
+ if (aColor) {
+ [self setColor:[NSColor colorWithDisplayP3Red:((aColor >> 16) & 0xFF) / 255.0
+ green:((aColor >> 8) & 0xFF) / 255.0
+ blue:((aColor)&0xFF) / 255.0
+ alpha:1.0]];
+ }
+ if (aChildren) {
+ uint32_t itemCount = 0;
+ aChildren->GetLength(&itemCount);
+ NSMutableArray* orderedChildren = [NSMutableArray arrayWithCapacity:itemCount];
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> child = do_QueryElementAt(aChildren, i);
+ if (!child) {
+ continue;
+ }
+ TouchBarInput* convertedChild = [[TouchBarInput alloc] initWithXPCOM:child];
+ if (convertedChild) {
+ orderedChildren[i] = convertedChild;
+ }
+ }
+ [self setChildren:orderedChildren];
+ }
+ }
+
+ return self;
+}
+
+- (TouchBarInput*)initWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput {
+ nsAutoString keyStr;
+ nsresult rv = aInput->GetKey(keyStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsAutoString titleStr;
+ rv = aInput->GetTitle(titleStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsIURI> imageURI;
+ rv = aInput->GetImage(getter_AddRefs(imageURI));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsAutoString typeStr;
+ rv = aInput->GetType(typeStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsITouchBarInputCallback> callback;
+ rv = aInput->GetCallback(getter_AddRefs(callback));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ uint32_t colorInt;
+ rv = aInput->GetColor(&colorInt);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ bool disabled = false;
+ rv = aInput->GetDisabled(&disabled);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsIArray> children;
+ rv = aInput->GetChildren(getter_AddRefs(children));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ return [self initWithKey:nsCocoaUtils::ToNSString(keyStr)
+ title:nsCocoaUtils::ToNSString(titleStr)
+ imageURI:imageURI
+ type:nsCocoaUtils::ToNSString(typeStr)
+ callback:callback
+ color:colorInt
+ disabled:(BOOL)disabled
+ children:children];
+}
+
+- (void)releaseJSObjects {
+ if (mIcon) {
+ mIcon->Destroy();
+ mIcon = nil;
+ }
+ [self setCallback:nil];
+ [self setImageURI:nil];
+ for (TouchBarInput* child in mChildren) {
+ [child releaseJSObjects];
+ }
+}
+
+- (void)dealloc {
+ if (mIcon) {
+ mIcon->Destroy();
+ mIcon = nil;
+ }
+ [mType release];
+ [mChildren removeAllObjects];
+ [mChildren release];
+ [super dealloc];
+}
+
++ (NSTouchBarItemIdentifier)nativeIdentifierWithType:(NSString*)aType withKey:(NSString*)aKey {
+ NSTouchBarItemIdentifier identifier;
+ identifier = [kTouchBarBaseIdentifier stringByAppendingPathExtension:aType];
+ if (aKey) {
+ identifier = [identifier stringByAppendingPathExtension:aKey];
+ }
+ return identifier;
+}
+
++ (NSTouchBarItemIdentifier)nativeIdentifierWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput {
+ nsAutoString keyStr;
+ nsresult rv = aInput->GetKey(keyStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+ NSString* key = nsCocoaUtils::ToNSString(keyStr);
+
+ nsAutoString typeStr;
+ rv = aInput->GetType(typeStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+ NSString* type = nsCocoaUtils::ToNSString(typeStr);
+
+ return [TouchBarInput nativeIdentifierWithType:type withKey:key];
+}
+
++ (NSTouchBarItemIdentifier)shareScrubberIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:@"scrubber" withKey:@"share"];
+}
+
++ (NSTouchBarItemIdentifier)searchPopoverIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:@"popover" withKey:@"search-popover"];
+}
+
+@end
diff --git a/widget/cocoa/nsTouchBarInputIcon.h b/widget/cocoa/nsTouchBarInputIcon.h
new file mode 100644
index 0000000000..115d7a0075
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInputIcon.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Retrieves and displays icons on the macOS Touch Bar.
+ */
+
+#ifndef nsTouchBarInputIcon_h_
+#define nsTouchBarInputIcon_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/widget/IconLoader.h"
+#include "nsTouchBarInput.h"
+#include "nsTouchBarNativeAPIDefines.h"
+
+using namespace mozilla::dom;
+
+class nsIURI;
+class nsIPrincipal;
+class imgRequestProxy;
+
+namespace mozilla::dom {
+class Document;
+}
+
+class nsTouchBarInputIcon : public mozilla::widget::IconLoader::Listener {
+ public:
+ explicit nsTouchBarInputIcon(RefPtr<Document> aDocument,
+ TouchBarInput* aInput, NSTouchBarItem* aItem);
+
+ NS_INLINE_DECL_REFCOUNTING(nsTouchBarInputIcon)
+
+ private:
+ virtual ~nsTouchBarInputIcon();
+
+ public:
+ // SetupIcon succeeds if it was able to set up the icon, or if there should
+ // be no icon, in which case it clears any existing icon but still succeeds.
+ nsresult SetupIcon(nsCOMPtr<nsIURI> aIconURI);
+
+ // Implements this method for mozilla::widget::IconLoader::Listener.
+ // Called once the icon load is complete.
+ nsresult OnComplete(imgIContainer* aImage) override;
+
+ // Unless we take precautions, we may outlive the object that created us
+ // (mTouchBar, which owns our native menu item (mTouchBarInput)).
+ // Destroy() should be called from mTouchBar's destructor to prevent
+ // this from happening.
+ void Destroy();
+
+ void ReleaseJSObjects();
+
+ protected:
+ RefPtr<Document> mDocument;
+ bool mSetIcon;
+ NSButton* mButton;
+ // We accept a mShareScrubber only as a special case since
+ // NSSharingServicePickerTouchBarItem does not expose an NSButton* on which we
+ // can set the `image` property.
+ NSSharingServicePickerTouchBarItem* mShareScrubber;
+ // We accept a popover only as a special case.
+ NSPopoverTouchBarItem* mPopoverItem;
+ // The icon loader object should never outlive its creating
+ // nsTouchBarInputIcon object.
+ RefPtr<mozilla::widget::IconLoader> mIconLoader;
+};
+
+#endif // nsTouchBarInputIcon_h_
diff --git a/widget/cocoa/nsTouchBarInputIcon.mm b/widget/cocoa/nsTouchBarInputIcon.mm
new file mode 100644
index 0000000000..ac9e907c8e
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInputIcon.mm
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/*
+ * Retrieves and displays icons on the macOS Touch Bar.
+ */
+
+#include "nsTouchBarInputIcon.h"
+
+#include "MOZIconHelper.h"
+#include "mozilla/dom/Document.h"
+#include "nsCocoaUtils.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsINode.h"
+#include "nsNameSpaceManager.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::widget::IconLoader;
+
+static const uint32_t kIconHeight = 16;
+static const CGFloat kHiDPIScalingFactor = 2.0f;
+
+nsTouchBarInputIcon::nsTouchBarInputIcon(RefPtr<Document> aDocument, TouchBarInput* aInput,
+ NSTouchBarItem* aItem)
+ : mDocument(aDocument), mSetIcon(false), mButton(nil), mShareScrubber(nil), mPopoverItem(nil) {
+ if ([[aInput nativeIdentifier] isEqualToString:[TouchBarInput shareScrubberIdentifier]]) {
+ mShareScrubber = (NSSharingServicePickerTouchBarItem*)aItem;
+ } else if ([aInput baseType] == TouchBarInputBaseType::kPopover) {
+ mPopoverItem = (NSPopoverTouchBarItem*)aItem;
+ } else if ([aInput baseType] == TouchBarInputBaseType::kButton ||
+ [aInput baseType] == TouchBarInputBaseType::kMainButton) {
+ mButton = (NSButton*)[aItem view];
+ } else {
+ NS_ERROR("Incompatible Touch Bar input passed to nsTouchBarInputIcon.");
+ }
+ aInput = nil;
+ MOZ_COUNT_CTOR(nsTouchBarInputIcon);
+}
+
+nsTouchBarInputIcon::~nsTouchBarInputIcon() {
+ Destroy();
+ MOZ_COUNT_DTOR(nsTouchBarInputIcon);
+}
+
+// Called from nsTouchBar's destructor, to prevent us from outliving it
+// (as might otherwise happen if calls to our imgINotificationObserver methods
+// are still outstanding). nsTouchBar owns our mTouchBarInput.
+void nsTouchBarInputIcon::Destroy() {
+ ReleaseJSObjects();
+ if (mIconLoader) {
+ mIconLoader->Destroy();
+ mIconLoader = nullptr;
+ }
+
+ mButton = nil;
+ mShareScrubber = nil;
+ mPopoverItem = nil;
+}
+
+nsresult nsTouchBarInputIcon::SetupIcon(nsCOMPtr<nsIURI> aIconURI) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We might not have a document if the Touch Bar tries to update when the main
+ // window is closed.
+ if (!mDocument) {
+ return NS_OK;
+ }
+
+ if (!(mButton || mShareScrubber || mPopoverItem)) {
+ NS_ERROR("No Touch Bar input provided.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mIconLoader) {
+ mIconLoader = new IconLoader(this);
+ }
+
+ if (!mSetIcon) {
+ // Load placeholder icon.
+ NSSize iconSize = NSMakeSize(kIconHeight, kIconHeight);
+ NSImage* placeholder = [MOZIconHelper placeholderIconWithSize:iconSize];
+ [mButton setImage:placeholder];
+ [mShareScrubber setButtonImage:placeholder];
+ [mPopoverItem setCollapsedRepresentationImage:placeholder];
+ }
+
+ nsresult rv = mIconLoader->LoadIcon(aIconURI, mDocument, true /* aIsInternalIcon */);
+ if (NS_FAILED(rv)) {
+ // There is no icon for this menu item, as an error occurred while loading it.
+ // An icon might have been set earlier or the place holder icon may have
+ // been set. Clear it.
+ [mButton setImage:nil];
+ [mShareScrubber setButtonImage:nil];
+ [mPopoverItem setCollapsedRepresentationImage:nil];
+ }
+
+ mSetIcon = true;
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsTouchBarInputIcon::ReleaseJSObjects() { mDocument = nil; }
+
+//
+// mozilla::widget::IconLoader::Listener
+//
+
+nsresult nsTouchBarInputIcon::OnComplete(imgIContainer* aImage) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ // We ask only for the HiDPI images since all Touch Bars are Retina
+ // displays and we have no need for icons @1x.
+ NSImage* image = [MOZIconHelper iconImageFromImageContainer:aImage
+ withSize:NSMakeSize(kIconHeight, kIconHeight)
+ presContext:nullptr
+ computedStyle:nullptr
+ scaleFactor:kHiDPIScalingFactor];
+ [mButton setImage:image];
+ [mShareScrubber setButtonImage:image];
+ [mPopoverItem setCollapsedRepresentationImage:image];
+
+ mIconLoader->Destroy();
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
diff --git a/widget/cocoa/nsTouchBarNativeAPIDefines.h b/widget/cocoa/nsTouchBarNativeAPIDefines.h
new file mode 100644
index 0000000000..1317a1bc79
--- /dev/null
+++ b/widget/cocoa/nsTouchBarNativeAPIDefines.h
@@ -0,0 +1,67 @@
+/* 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/. */
+
+#ifndef nsTouchBarNativeAPIDefines_h
+#define nsTouchBarNativeAPIDefines_h
+
+#import <Cocoa/Cocoa.h>
+
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+@interface NSApplication (TouchBarMenu)
+- (IBAction)toggleTouchBarCustomizationPalette:(id)sender;
+@end
+
+typedef NSString* NSTouchBarItemIdentifier;
+__attribute__((weak_import))
+@interface NSTouchBarItem : NSObject
+@property(readonly) NSView* view;
+@property(readonly) NSString* customizationLabel;
+- (instancetype)initWithIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+@end
+
+@protocol NSSharingServicePickerTouchBarItemDelegate
+@end
+
+__attribute__((weak_import))
+@interface NSSharingServicePickerTouchBarItem : NSTouchBarItem
+@property(strong) id<NSSharingServicePickerTouchBarItemDelegate> delegate;
+@property(strong) NSImage* buttonImage;
+@end
+
+__attribute__((weak_import))
+@interface NSCustomTouchBarItem : NSTouchBarItem
+@property(strong) NSView* view;
+@property(strong) NSString* customizationLabel;
+@end
+
+@protocol NSTouchBarDelegate
+@end
+
+typedef NSString* NSTouchBarCustomizationIdentifier;
+__attribute__((weak_import))
+@interface NSTouchBar : NSObject
+@property(strong) NSArray<NSTouchBarItemIdentifier>* defaultItemIdentifiers;
+@property(strong) id<NSTouchBarDelegate> delegate;
+@property(strong) NSTouchBarCustomizationIdentifier customizationIdentifier;
+@property(strong) NSArray<NSTouchBarItemIdentifier>* customizationAllowedItemIdentifiers;
+- (NSTouchBarItem*)itemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+@end
+
+__attribute__((weak_import))
+@interface NSPopoverTouchBarItem : NSTouchBarItem
+@property(strong) NSString* customizationLabel;
+@property(strong) NSView* collapsedRepresentation;
+@property(strong) NSImage* collapsedRepresentationImage;
+@property(strong) NSString* collapsedRepresentationLabel;
+@property(strong) NSTouchBar* popoverTouchBar;
+@property BOOL showsCloseButton;
+- (void)showPopover:(id)sender;
+- (void)dismissPopover:(id)sender;
+@end
+
+@interface NSButton (TouchBarButton)
+@property(strong) NSColor* bezelColor;
+@end
+#endif // !defined(MAC_OS_X_VERSION_10_12_2)
+#endif // nsTouchBarNativeAPIDefines_h
diff --git a/widget/cocoa/nsTouchBarUpdater.h b/widget/cocoa/nsTouchBarUpdater.h
new file mode 100644
index 0000000000..38039f69a0
--- /dev/null
+++ b/widget/cocoa/nsTouchBarUpdater.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsTouchBarUpdater_h_
+#define nsTouchBarUpdater_h_
+
+#include "nsITouchBarUpdater.h"
+#include "nsCocoaWindow.h"
+
+class nsTouchBarUpdater : public nsITouchBarUpdater {
+ public:
+ nsTouchBarUpdater() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITOUCHBARUPDATER
+
+ protected:
+ virtual ~nsTouchBarUpdater() {}
+ BaseWindow* GetCocoaWindow(nsIBaseWindow* aWindow);
+};
+
+#endif // nsTouchBarUpdater_h_
diff --git a/widget/cocoa/nsTouchBarUpdater.mm b/widget/cocoa/nsTouchBarUpdater.mm
new file mode 100644
index 0000000000..6b4b25f992
--- /dev/null
+++ b/widget/cocoa/nsTouchBarUpdater.mm
@@ -0,0 +1,116 @@
+/* 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 <Cocoa/Cocoa.h>
+
+#include "nsTouchBar.h"
+#include "nsTouchBarInput.h"
+#include "nsTouchBarUpdater.h"
+#include "nsTouchBarNativeAPIDefines.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIWidget.h"
+
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+@interface BaseWindow (NSTouchBarProvider)
+@property(strong) NSTouchBar* touchBar;
+@end
+#endif
+
+NS_IMPL_ISUPPORTS(nsTouchBarUpdater, nsITouchBarUpdater);
+
+NS_IMETHODIMP
+nsTouchBarUpdater::UpdateTouchBarInputs(nsIBaseWindow* aWindow,
+ const nsTArray<RefPtr<nsITouchBarInput>>& aInputs) {
+ if (!sTouchBarIsInitialized || !aWindow) {
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsTouchBarUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if ([cocoaWin respondsToSelector:@selector(touchBar)]) {
+ size_t itemCount = aInputs.Length();
+ for (size_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> input(aInputs.ElementAt(i));
+ if (!input) {
+ continue;
+ }
+
+ NSTouchBarItemIdentifier newIdentifier = [TouchBarInput nativeIdentifierWithXPCOM:input];
+ // We don't support updating the Share scrubber since it's a special
+ // Apple-made component that behaves differently from the other inputs.
+ if ([newIdentifier isEqualToString:[TouchBarInput nativeIdentifierWithType:@"scrubber"
+ withKey:@"share"]]) {
+ continue;
+ }
+
+ TouchBarInput* convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
+ [(nsTouchBar*)cocoaWin.touchBar updateItem:convertedInput];
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::ShowPopover(nsIBaseWindow* aWindow, nsITouchBarInput* aPopover, bool aShowing) {
+ if (!sTouchBarIsInitialized || !aPopover || !aWindow) {
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsTouchBarUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if ([cocoaWin respondsToSelector:@selector(touchBar)]) {
+ // We don't need to completely reinitialize the popover. We only need its
+ // identifier to look it up in [nsTouchBar mappedLayoutItems].
+ NSTouchBarItemIdentifier popoverIdentifier = [TouchBarInput nativeIdentifierWithXPCOM:aPopover];
+
+ TouchBarInput* popoverItem =
+ [[(nsTouchBar*)cocoaWin.touchBar mappedLayoutItems] objectForKey:popoverIdentifier];
+
+ [(nsTouchBar*)cocoaWin.touchBar showPopover:popoverItem showing:aShowing];
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::EnterCustomizeMode() {
+ [NSApp toggleTouchBarCustomizationPalette:(id)this];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::IsTouchBarInitialized(bool* aResult) {
+ *aResult = sTouchBarIsInitialized;
+ return NS_OK;
+}
+
+BaseWindow* nsTouchBarUpdater::GetCocoaWindow(nsIBaseWindow* aWindow) {
+ nsCOMPtr<nsIWidget> widget = nullptr;
+ aWindow->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ return nil;
+ }
+ BaseWindow* cocoaWin = (BaseWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!cocoaWin) {
+ return nil;
+ }
+ return cocoaWin;
+}
+
+// NOTE: This method is for internal unit tests only.
+NS_IMETHODIMP
+nsTouchBarUpdater::SetTouchBarInitialized(bool aIsInitialized) {
+ sTouchBarIsInitialized = aIsInitialized;
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsUserIdleServiceX.h b/widget/cocoa/nsUserIdleServiceX.h
new file mode 100644
index 0000000000..cdd5130a33
--- /dev/null
+++ b/widget/cocoa/nsUserIdleServiceX.h
@@ -0,0 +1,30 @@
+/* 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/. */
+
+#ifndef nsUserIdleServiceX_h_
+#define nsUserIdleServiceX_h_
+
+#include "nsUserIdleService.h"
+
+class nsUserIdleServiceX : public nsUserIdleService {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceX, nsUserIdleService)
+
+ bool PollIdleTime(uint32_t* aIdleTime) override;
+
+ static already_AddRefed<nsUserIdleServiceX> GetInstance() {
+ RefPtr<nsUserIdleService> idleService = nsUserIdleService::GetInstance();
+ if (!idleService) {
+ idleService = new nsUserIdleServiceX();
+ }
+
+ return idleService.forget().downcast<nsUserIdleServiceX>();
+ }
+
+ protected:
+ nsUserIdleServiceX() {}
+ virtual ~nsUserIdleServiceX() {}
+};
+
+#endif // nsUserIdleServiceX_h_
diff --git a/widget/cocoa/nsUserIdleServiceX.mm b/widget/cocoa/nsUserIdleServiceX.mm
new file mode 100644
index 0000000000..bedaf0773b
--- /dev/null
+++ b/widget/cocoa/nsUserIdleServiceX.mm
@@ -0,0 +1,58 @@
+/* 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/. */
+
+#include "nsUserIdleServiceX.h"
+#include "nsObjCExceptions.h"
+#import <Foundation/Foundation.h>
+
+bool nsUserIdleServiceX::PollIdleTime(uint32_t* aIdleTime) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ kern_return_t rval;
+ mach_port_t masterPort;
+
+ rval = IOMasterPort(kIOMasterPortDefault, &masterPort);
+ if (rval != KERN_SUCCESS) return false;
+
+ io_iterator_t hidItr;
+ rval = IOServiceGetMatchingServices(masterPort, IOServiceMatching("IOHIDSystem"), &hidItr);
+
+ if (rval != KERN_SUCCESS) return false;
+ NS_ASSERTION(hidItr, "Our iterator is null, but it ought not to be!");
+
+ io_registry_entry_t entry = IOIteratorNext(hidItr);
+ NS_ASSERTION(entry, "Our IO Registry Entry is null, but it shouldn't be!");
+
+ IOObjectRelease(hidItr);
+
+ NSMutableDictionary* hidProps;
+ rval = IORegistryEntryCreateCFProperties(entry, (CFMutableDictionaryRef*)&hidProps,
+ kCFAllocatorDefault, 0);
+ if (rval != KERN_SUCCESS) return false;
+ NS_ASSERTION(hidProps, "HIDProperties is null, but no error was returned.");
+ [hidProps autorelease];
+
+ id idleObj = [hidProps objectForKey:@"HIDIdleTime"];
+ NS_ASSERTION([idleObj isKindOfClass:[NSData class]] || [idleObj isKindOfClass:[NSNumber class]],
+ "What we got for the idle object is not what we expect!");
+
+ uint64_t time;
+ if ([idleObj isKindOfClass:[NSData class]])
+ [idleObj getBytes:&time length:sizeof(time)];
+ else
+ time = [idleObj unsignedLongLongValue];
+
+ IOObjectRelease(entry);
+
+ // convert to ms from ns
+ time /= 1000000;
+ if (time > UINT32_MAX) // Overflow will occur
+ return false;
+
+ *aIdleTime = static_cast<uint32_t>(time);
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
diff --git a/widget/cocoa/nsWidgetFactory.h b/widget/cocoa/nsWidgetFactory.h
new file mode 100644
index 0000000000..ce3ca756db
--- /dev/null
+++ b/widget/cocoa/nsWidgetFactory.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+// This file contains forward declarations for classes defined in static
+// components. The appropriate headers for those types cannot be included in
+// the generated static component code directly.
+
+#include "nsID.h"
+
+namespace mozilla {
+class OSXNotificationCenter;
+} // namespace mozilla
+
+namespace mozilla::widget {
+class ScreenManager;
+}
+
+class nsClipboardHelper;
+class nsColorPicker;
+class nsDeviceContextSpecX;
+class nsDragService;
+class nsFilePicker;
+class nsHTMLFormatConverter;
+class nsMacDockSupport;
+class nsMacFinderProgress;
+class nsMacSharingService;
+class nsMacUserActivityUpdater;
+class nsMacWebAppUtils;
+class nsPrintDialogServiceX;
+class nsPrintSettingsServiceX;
+class nsPrinterListCUPS;
+class nsSound;
+class nsStandaloneNativeMenu;
+class nsSystemStatusBarCocoa;
+class nsTouchBarUpdater;
+class nsTransferable;
+class nsUserIdleServiceX;
+
+nsresult nsAppShellConstructor(const nsIID&, void**);
+
+void nsWidgetCocoaModuleCtor();
+void nsWidgetCocoaModuleDtor();
diff --git a/widget/cocoa/nsWidgetFactory.mm b/widget/cocoa/nsWidgetFactory.mm
new file mode 100644
index 0000000000..3126b8c225
--- /dev/null
+++ b/widget/cocoa/nsWidgetFactory.mm
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#include "nsISupports.h"
+#include "mozilla/Components.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/WidgetUtils.h"
+
+#include "nsWidgetsCID.h"
+
+#include "nsChildView.h"
+#include "nsAppShell.h"
+#include "nsAppShellSingleton.h"
+#include "nsFilePicker.h"
+#include "nsColorPicker.h"
+
+#include "nsClipboard.h"
+#include "nsClipboardHelper.h"
+#include "HeadlessClipboard.h"
+#include "gfxPlatform.h"
+#include "nsTransferable.h"
+#include "nsHTMLFormatConverter.h"
+#include "nsDragService.h"
+#include "nsToolkit.h"
+
+#include "nsLookAndFeel.h"
+
+#include "nsSound.h"
+#include "nsUserIdleServiceX.h"
+#include "NativeKeyBindings.h"
+#include "OSXNotificationCenter.h"
+
+#include "nsDeviceContextSpecX.h"
+#include "nsPrinterListCUPS.h"
+#include "nsPrintSettingsServiceX.h"
+#include "nsPrintDialogX.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/widget/ScreenManager.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+NS_IMPL_COMPONENT_FACTORY(nsIClipboard) {
+ nsCOMPtr<nsIClipboard> inst;
+ if (gfxPlatform::IsHeadless()) {
+ inst = new HeadlessClipboard();
+ } else {
+ inst = new nsClipboard();
+ }
+
+ return inst.forget();
+}
+
+#define MAKE_GENERIC_CTOR(class_, iface_) \
+ NS_IMPL_COMPONENT_FACTORY(class_) { \
+ RefPtr inst = new class_(); \
+ return inst.forget().downcast<iface_>(); \
+ }
+
+#define MAKE_GENERIC_CTOR_INIT(class_, iface_, init_) \
+ NS_IMPL_COMPONENT_FACTORY(class_) { \
+ RefPtr inst = new class_(); \
+ if (NS_SUCCEEDED(inst->init_())) { \
+ return inst.forget().downcast<iface_>(); \
+ } \
+ return nullptr; \
+ }
+
+#define MAKE_GENERIC_SINGLETON_CTOR(iface_, func_) \
+ NS_IMPL_COMPONENT_FACTORY(iface_) { return func_(); }
+
+MAKE_GENERIC_CTOR(nsFilePicker, nsIFilePicker)
+MAKE_GENERIC_CTOR(nsColorPicker, nsIColorPicker)
+MAKE_GENERIC_CTOR(nsSound, nsISound)
+MAKE_GENERIC_CTOR(nsTransferable, nsITransferable)
+MAKE_GENERIC_CTOR(nsHTMLFormatConverter, nsIFormatConverter)
+MAKE_GENERIC_CTOR(nsClipboardHelper, nsIClipboardHelper)
+MAKE_GENERIC_CTOR(nsDragService, nsIDragService)
+MAKE_GENERIC_CTOR(nsDeviceContextSpecX, nsIDeviceContextSpec)
+MAKE_GENERIC_CTOR(nsPrinterListCUPS, nsIPrinterList)
+MAKE_GENERIC_CTOR_INIT(nsPrintSettingsServiceX, nsIPrintSettingsService, Init)
+MAKE_GENERIC_CTOR_INIT(nsPrintDialogServiceX, nsIPrintDialogService, Init)
+MAKE_GENERIC_SINGLETON_CTOR(nsUserIdleServiceX, nsUserIdleServiceX::GetInstance)
+MAKE_GENERIC_SINGLETON_CTOR(ScreenManager, ScreenManager::GetAddRefedSingleton)
+MAKE_GENERIC_CTOR_INIT(OSXNotificationCenter, nsIAlertsService, Init)
+
+#include "nsMacDockSupport.h"
+MAKE_GENERIC_CTOR(nsMacDockSupport, nsIMacDockSupport)
+
+#include "nsMacFinderProgress.h"
+MAKE_GENERIC_CTOR(nsMacFinderProgress, nsIMacFinderProgress)
+
+#include "nsMacSharingService.h"
+MAKE_GENERIC_CTOR(nsMacSharingService, nsIMacSharingService)
+
+#include "nsMacUserActivityUpdater.h"
+MAKE_GENERIC_CTOR(nsMacUserActivityUpdater, nsIMacUserActivityUpdater)
+
+#include "nsMacWebAppUtils.h"
+MAKE_GENERIC_CTOR(nsMacWebAppUtils, nsIMacWebAppUtils)
+
+#include "nsStandaloneNativeMenu.h"
+MAKE_GENERIC_CTOR(nsStandaloneNativeMenu, nsIStandaloneNativeMenu)
+
+#include "nsSystemStatusBarCocoa.h"
+MAKE_GENERIC_CTOR(nsSystemStatusBarCocoa, nsISystemStatusBar)
+
+#include "nsTouchBarUpdater.h"
+MAKE_GENERIC_CTOR(nsTouchBarUpdater, nsITouchBarUpdater)
+
+void nsWidgetCocoaModuleCtor() { nsAppShellInit(); }
+
+void nsWidgetCocoaModuleDtor() {
+ // Shutdown all XP level widget classes.
+ WidgetUtils::Shutdown();
+
+ NativeKeyBindings::Shutdown();
+ nsLookAndFeel::Shutdown();
+ nsToolkit::Shutdown();
+ nsAppShellShutdown();
+}
diff --git a/widget/cocoa/nsWindowMap.h b/widget/cocoa/nsWindowMap.h
new file mode 100644
index 0000000000..9326415df7
--- /dev/null
+++ b/widget/cocoa/nsWindowMap.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#ifndef nsWindowMap_h_
+#define nsWindowMap_h_
+
+#import <Cocoa/Cocoa.h>
+
+// WindowDataMap
+//
+// In both mozilla and embedding apps, we need to have a place to put
+// per-top-level-window logic and data, to handle such things as IME
+// commit when the window gains/loses focus. We can't use a window
+// delegate, because an embeddor probably already has one. Nor can we
+// subclass NSWindow, again because we can't impose that burden on the
+// embeddor.
+//
+// So we have a global map of NSWindow -> TopLevelWindowData, and set
+// up TopLevelWindowData as a notification observer etc.
+
+@interface WindowDataMap : NSObject {
+ @private
+ NSMutableDictionary* mWindowMap; // dict of TopLevelWindowData keyed by address of NSWindow
+}
+
++ (WindowDataMap*)sharedWindowDataMap;
+
+- (void)ensureDataForWindow:(NSWindow*)inWindow;
+- (id)dataForWindow:(NSWindow*)inWindow;
+
+// set data for a given window. inData is retained (and any previously set data
+// is released).
+- (void)setData:(id)inData forWindow:(NSWindow*)inWindow;
+
+// remove the data for the given window. the data is released.
+- (void)removeDataForWindow:(NSWindow*)inWindow;
+
+@end
+
+@class ChildView;
+
+// TopLevelWindowData
+//
+// Class to hold per-window data, and handle window state changes.
+
+@interface TopLevelWindowData : NSObject {
+ @private
+}
+
+ - (id)initWithWindow:(NSWindow*)inWindow;
+ + (void)activateInWindow:(NSWindow*)aWindow;
+ + (void)deactivateInWindow:(NSWindow*)aWindow;
+ + (void)activateInWindowViews:(NSWindow*)aWindow;
+ + (void)deactivateInWindowViews:(NSWindow*)aWindow;
+
+ @end
+
+#endif // nsWindowMap_h_
diff --git a/widget/cocoa/nsWindowMap.mm b/widget/cocoa/nsWindowMap.mm
new file mode 100644
index 0000000000..010ed76cd7
--- /dev/null
+++ b/widget/cocoa/nsWindowMap.mm
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+#include "nsWindowMap.h"
+#include "nsObjCExceptions.h"
+#include "nsChildView.h"
+#include "nsCocoaWindow.h"
+
+@interface WindowDataMap (Private)
+
+- (NSString*)keyForWindow:(NSWindow*)inWindow;
+
+@end
+
+@interface TopLevelWindowData (Private)
+
+- (void)windowResignedKey:(NSNotification*)inNotification;
+- (void)windowBecameKey:(NSNotification*)inNotification;
+- (void)windowWillClose:(NSNotification*)inNotification;
+
+@end
+
+#pragma mark -
+
+@implementation WindowDataMap
+
++ (WindowDataMap*)sharedWindowDataMap {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ static WindowDataMap* sWindowMap = nil;
+ if (!sWindowMap) sWindowMap = [[WindowDataMap alloc] init];
+
+ return sWindowMap;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)init {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ mWindowMap = [[NSMutableDictionary alloc] initWithCapacity:10];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap release];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)ensureDataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!inWindow || [self dataForWindow:inWindow]) return;
+
+ TopLevelWindowData* windowData = [[TopLevelWindowData alloc] initWithWindow:inWindow];
+ [self setData:windowData forWindow:inWindow]; // takes ownership
+ [windowData release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (id)dataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mWindowMap objectForKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)setData:(id)inData forWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap setObject:inData forKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)removeDataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap removeObjectForKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSString*)keyForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSString stringWithFormat:@"%p", inWindow];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+@end
+
+// TopLevelWindowData
+//
+// This class holds data about top-level windows. We can't use a window
+// delegate, because an embedder may already have one.
+
+@implementation TopLevelWindowData
+
+- (id)initWithWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowBecameKey:)
+ name:NSWindowDidBecomeKeyNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowResignedKey:)
+ name:NSWindowDidResignKeyNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowBecameMain:)
+ name:NSWindowDidBecomeMainNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowResignedMain:)
+ name:NSWindowDidResignMainNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:inWindow];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// As best I can tell, if the notification's object has a corresponding
+// top-level widget (an nsCocoaWindow object), it has a delegate (set in
+// nsCocoaWindow::StandardCreate()) of class WindowDelegate, and otherwise
+// not (Camino didn't use top-level widgets (nsCocoaWindow objects) --
+// only child widgets (nsChildView objects)). (The notification is sent
+// to windowBecameKey: or windowBecameMain: below.)
+//
+// For use with clients that (like Firefox) do use top-level widgets (and
+// have NSWindow delegates of class WindowDelegate).
++ (void)activateInWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ WindowDelegate* delegate = (WindowDelegate*)[aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return;
+
+ if ([delegate toplevelActiveState]) return;
+ [delegate sendToplevelActivateEvents];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// See comments above activateInWindow:
+//
+// If we're using top-level widgets (nsCocoaWindow objects), we send them
+// NS_DEACTIVATE events (which propagate to child widgets (nsChildView
+// objects) via nsWebShellWindow::HandleEvent()).
+//
+// For use with clients that (like Firefox) do use top-level widgets (and
+// have NSWindow delegates of class WindowDelegate).
++ (void)deactivateInWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ WindowDelegate* delegate = (WindowDelegate*)[aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return;
+
+ if (![delegate toplevelActiveState]) return;
+ [delegate sendToplevelDeactivateEvents];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// For use with clients that (like Camino) don't use top-level widgets (and
+// don't have NSWindow delegates of class WindowDelegate).
++ (void)activateInWindowViews:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ id firstResponder = [aWindow firstResponder];
+ if ([firstResponder isKindOfClass:[ChildView class]]) [firstResponder viewsWindowDidBecomeKey];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// For use with clients that (like Camino) don't use top-level widgets (and
+// don't have NSWindow delegates of class WindowDelegate).
++ (void)deactivateInWindowViews:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ id firstResponder = [aWindow firstResponder];
+ if ([firstResponder isKindOfClass:[ChildView class]]) [firstResponder viewsWindowDidResignKey];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// We make certain exceptions for top-level windows in non-embedders (see
+// comment above windowBecameMain below). And we need (elsewhere) to guard
+// against sending duplicate events. But in general the NS_ACTIVATE event
+// should be sent when a native window becomes key, and the NS_DEACTIVATE
+// event should be sent when it resignes key.
+- (void)windowBecameKey:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
+ [TopLevelWindowData activateInWindowViews:window];
+ } else if ([window isSheet] || [NSApp modalWindow]) {
+ [TopLevelWindowData activateInWindow:window];
+ }
+}
+
+- (void)windowResignedKey:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
+ [TopLevelWindowData deactivateInWindowViews:window];
+ } else if ([window isSheet] || [NSApp modalWindow]) {
+ [TopLevelWindowData deactivateInWindow:window];
+ }
+}
+
+// The appearance of a top-level window depends on its main state (not its key
+// state). So (for non-embedders) we need to ensure that a top-level window
+// is main when an NS_ACTIVATE event is sent to Gecko for it.
+- (void)windowBecameMain:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ // Don't send events to a top-level window that has a sheet/modal-window open
+ // above it -- as far as Gecko is concerned, it's inactive, and stays so until
+ // the sheet/modal-window closes.
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]] &&
+ ![window attachedSheet] && ![NSApp modalWindow])
+ [TopLevelWindowData activateInWindow:window];
+}
+
+- (void)windowResignedMain:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]] && ![window attachedSheet])
+ [TopLevelWindowData deactivateInWindow:window];
+}
+
+- (void)windowWillClose:(NSNotification*)inNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // postpone our destruction
+ [[self retain] autorelease];
+
+ // remove ourselves from the window map (which owns us)
+ [[WindowDataMap sharedWindowDataMap] removeDataForWindow:[inNotification object]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/resources/MainMenu.nib/classes.nib b/widget/cocoa/resources/MainMenu.nib/classes.nib
new file mode 100644
index 0000000000..b9b4b09f6b
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/classes.nib
@@ -0,0 +1,4 @@
+{
+ IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; });
+ IBVersion = 1;
+} \ No newline at end of file
diff --git a/widget/cocoa/resources/MainMenu.nib/info.nib b/widget/cocoa/resources/MainMenu.nib/info.nib
new file mode 100644
index 0000000000..bcf3ace841
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/info.nib
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBDocumentLocation</key>
+ <string>159 127 356 240 0 0 1920 1178 </string>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>29</key>
+ <string>413 971 130 44 0 0 1920 1178 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>443.0</string>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>29</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>8F46</string>
+</dict>
+</plist>
diff --git a/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 0000000000..16b3f7e523
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib
Binary files differ