diff options
Diffstat (limited to 'widget/cocoa')
179 files changed, 43514 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..801c37c51f --- /dev/null +++ b/widget/cocoa/GfxInfo.h @@ -0,0 +1,99 @@ +/* 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 GetDesktopEnvironment(nsAString& aDesktopEnvironment) 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..0f4077a00e --- /dev/null +++ b/widget/cocoa/GfxInfo.mm @@ -0,0 +1,497 @@ +/* -*- 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 desktopEnvironment; */ +NS_IMETHODIMP +GfxInfo::GetDesktopEnvironment(nsAString& aDesktopEnvironment) { 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"); + } + 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; +} + +/* void fireTestProcess (); */ +NS_IMETHODIMP GfxInfo::FireTestProcess() { return NS_OK; } + +#endif diff --git a/widget/cocoa/MOZIconHelper.h b/widget/cocoa/MOZIconHelper.h new file mode 100644 index 0000000000..940768f56d --- /dev/null +++ b/widget/cocoa/MOZIconHelper.h @@ -0,0 +1,35 @@ +/* -*- 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 + subrect:(const nsIntRect&)aSubRect + 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..b7cbac5b68 --- /dev/null +++ b/widget/cocoa/MOZIconHelper.mm @@ -0,0 +1,91 @@ +/* -*- 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 + subrect:(const nsIntRect&)aSubRect + 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 the image region is invalid, don't draw the image to almost match + // the behavior of other platforms. + if (!aSubRect.IsEmpty() && (aSubRect.XMost() > origWidth || aSubRect.YMost() > origHeight)) { + return nil; + } + + bool createSubImage = + !aSubRect.IsEmpty() && !(aSubRect.x == 0 && aSubRect.y == 0 && aSubRect.width == origWidth && + aSubRect.height == origHeight); + + if (createSubImage) { + // If aRect is set using CSS, we need to slice a piece out of the + // overall image to use as the icon. + NSRect subRect = NSMakeRect(aSubRect.x, aSubRect.y, aSubRect.width, aSubRect.height); + NSImage* subImage = [NSImage imageWithSize:aSize + flipped:NO + drawingHandler:^BOOL(NSRect subImageRect) { + [image drawInRect:NSMakeRect(0, 0, aSize.width, aSize.height) + fromRect:subRect + operation:NSCompositingOperationCopy + fraction:1.0f]; + return YES; + }]; + image = subImage; + } + + // 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..6df63db07c --- /dev/null +++ b/widget/cocoa/MOZMenuOpeningCoordinator.h @@ -0,0 +1,52 @@ +/* -*- 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; + +// 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..00c4c8ba01 --- /dev/null +++ b/widget/cocoa/MOZMenuOpeningCoordinator.mm @@ -0,0 +1,199 @@ +/* -*- 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; +@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 { + 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; + 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]; + } @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 { + // 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 because it's the simplest: It works in all + // scenarios and it doesn't have the positioning drawbacks of 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; + // Create a synthetic event at the right location and open the menu [option 4]. + NSPoint locationInWindow = nsCocoaUtils::ConvertPointFromScreen(window, aPosition); + 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 { + // 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..a258c7900c --- /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; +} + +} +} 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..950e6edff1 --- /dev/null +++ b/widget/cocoa/NativeMenuMac.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/. */ + +#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) 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..960ee67c08 --- /dev/null +++ b/widget/cocoa/NativeMenuMac.mm @@ -0,0 +1,392 @@ +/* -*- 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.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) { + 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]; +} + +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, ¤tKeyEvent); + + 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, ¤tKeyEvent); + + 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 Binary files differnew file mode 100644 index 0000000000..c65924091a --- /dev/null +++ b/widget/cocoa/cursors/arrowN.png diff --git a/widget/cocoa/cursors/arrowN@2x.png b/widget/cocoa/cursors/arrowN@2x.png Binary files differnew file mode 100644 index 0000000000..496f856a1d --- /dev/null +++ b/widget/cocoa/cursors/arrowN@2x.png diff --git a/widget/cocoa/cursors/arrowS.png b/widget/cocoa/cursors/arrowS.png Binary files differnew file mode 100644 index 0000000000..3975e0837b --- /dev/null +++ b/widget/cocoa/cursors/arrowS.png diff --git a/widget/cocoa/cursors/arrowS@2x.png b/widget/cocoa/cursors/arrowS@2x.png Binary files differnew file mode 100644 index 0000000000..c7f817afd4 --- /dev/null +++ b/widget/cocoa/cursors/arrowS@2x.png diff --git a/widget/cocoa/cursors/cell.png b/widget/cocoa/cursors/cell.png Binary files differnew file mode 100644 index 0000000000..1400d93f6e --- /dev/null +++ b/widget/cocoa/cursors/cell.png diff --git a/widget/cocoa/cursors/cell@2x.png b/widget/cocoa/cursors/cell@2x.png Binary files differnew file mode 100644 index 0000000000..5a1543b16b --- /dev/null +++ b/widget/cocoa/cursors/cell@2x.png diff --git a/widget/cocoa/cursors/colResize.png b/widget/cocoa/cursors/colResize.png Binary files differnew file mode 100644 index 0000000000..ef4b24936b --- /dev/null +++ b/widget/cocoa/cursors/colResize.png diff --git a/widget/cocoa/cursors/colResize@2x.png b/widget/cocoa/cursors/colResize@2x.png Binary files differnew file mode 100644 index 0000000000..d868ee6b57 --- /dev/null +++ b/widget/cocoa/cursors/colResize@2x.png diff --git a/widget/cocoa/cursors/help.png b/widget/cocoa/cursors/help.png Binary files differnew file mode 100644 index 0000000000..7070b4b7bc --- /dev/null +++ b/widget/cocoa/cursors/help.png diff --git a/widget/cocoa/cursors/help@2x.png b/widget/cocoa/cursors/help@2x.png Binary files differnew file mode 100644 index 0000000000..7ddf157cd3 --- /dev/null +++ b/widget/cocoa/cursors/help@2x.png diff --git a/widget/cocoa/cursors/move.png b/widget/cocoa/cursors/move.png Binary files differnew file mode 100644 index 0000000000..f6291500a4 --- /dev/null +++ b/widget/cocoa/cursors/move.png diff --git a/widget/cocoa/cursors/move@2x.png b/widget/cocoa/cursors/move@2x.png Binary files differnew file mode 100644 index 0000000000..d094ce7a15 --- /dev/null +++ b/widget/cocoa/cursors/move@2x.png diff --git a/widget/cocoa/cursors/rowResize.png b/widget/cocoa/cursors/rowResize.png Binary files differnew file mode 100644 index 0000000000..7f68a4007c --- /dev/null +++ b/widget/cocoa/cursors/rowResize.png diff --git a/widget/cocoa/cursors/rowResize@2x.png b/widget/cocoa/cursors/rowResize@2x.png Binary files differnew file mode 100644 index 0000000000..e0987a3b95 --- /dev/null +++ b/widget/cocoa/cursors/rowResize@2x.png diff --git a/widget/cocoa/cursors/sizeNE.png b/widget/cocoa/cursors/sizeNE.png Binary files differnew file mode 100644 index 0000000000..fd71dea6ab --- /dev/null +++ b/widget/cocoa/cursors/sizeNE.png diff --git a/widget/cocoa/cursors/sizeNE@2x.png b/widget/cocoa/cursors/sizeNE@2x.png Binary files differnew file mode 100644 index 0000000000..400f5fe46f --- /dev/null +++ b/widget/cocoa/cursors/sizeNE@2x.png diff --git a/widget/cocoa/cursors/sizeNESW.png b/widget/cocoa/cursors/sizeNESW.png Binary files differnew file mode 100644 index 0000000000..5b2c300f4a --- /dev/null +++ b/widget/cocoa/cursors/sizeNESW.png diff --git a/widget/cocoa/cursors/sizeNESW@2x.png b/widget/cocoa/cursors/sizeNESW@2x.png Binary files differnew file mode 100644 index 0000000000..7299b98be1 --- /dev/null +++ b/widget/cocoa/cursors/sizeNESW@2x.png diff --git a/widget/cocoa/cursors/sizeNS.png b/widget/cocoa/cursors/sizeNS.png Binary files differnew file mode 100644 index 0000000000..12b1602f14 --- /dev/null +++ b/widget/cocoa/cursors/sizeNS.png diff --git a/widget/cocoa/cursors/sizeNS@2x.png b/widget/cocoa/cursors/sizeNS@2x.png Binary files differnew file mode 100644 index 0000000000..0dc7d15d75 --- /dev/null +++ b/widget/cocoa/cursors/sizeNS@2x.png diff --git a/widget/cocoa/cursors/sizeNW.png b/widget/cocoa/cursors/sizeNW.png Binary files differnew file mode 100644 index 0000000000..57d270e0db --- /dev/null +++ b/widget/cocoa/cursors/sizeNW.png diff --git a/widget/cocoa/cursors/sizeNW@2x.png b/widget/cocoa/cursors/sizeNW@2x.png Binary files differnew file mode 100644 index 0000000000..312ee61ce4 --- /dev/null +++ b/widget/cocoa/cursors/sizeNW@2x.png diff --git a/widget/cocoa/cursors/sizeNWSE.png b/widget/cocoa/cursors/sizeNWSE.png Binary files differnew file mode 100644 index 0000000000..d33c2486e4 --- /dev/null +++ b/widget/cocoa/cursors/sizeNWSE.png diff --git a/widget/cocoa/cursors/sizeNWSE@2x.png b/widget/cocoa/cursors/sizeNWSE@2x.png Binary files differnew file mode 100644 index 0000000000..ecf1438265 --- /dev/null +++ b/widget/cocoa/cursors/sizeNWSE@2x.png diff --git a/widget/cocoa/cursors/sizeSE.png b/widget/cocoa/cursors/sizeSE.png Binary files differnew file mode 100644 index 0000000000..1689138419 --- /dev/null +++ b/widget/cocoa/cursors/sizeSE.png diff --git a/widget/cocoa/cursors/sizeSE@2x.png b/widget/cocoa/cursors/sizeSE@2x.png Binary files differnew file mode 100644 index 0000000000..7abce00fd5 --- /dev/null +++ b/widget/cocoa/cursors/sizeSE@2x.png diff --git a/widget/cocoa/cursors/sizeSW.png b/widget/cocoa/cursors/sizeSW.png Binary files differnew file mode 100644 index 0000000000..5eadafb054 --- /dev/null +++ b/widget/cocoa/cursors/sizeSW.png diff --git a/widget/cocoa/cursors/sizeSW@2x.png b/widget/cocoa/cursors/sizeSW@2x.png Binary files differnew file mode 100644 index 0000000000..b9ad862aa5 --- /dev/null +++ b/widget/cocoa/cursors/sizeSW@2x.png diff --git a/widget/cocoa/cursors/vtIBeam.png b/widget/cocoa/cursors/vtIBeam.png Binary files differnew file mode 100644 index 0000000000..4609922319 --- /dev/null +++ b/widget/cocoa/cursors/vtIBeam.png diff --git a/widget/cocoa/cursors/vtIBeam@2x.png b/widget/cocoa/cursors/vtIBeam@2x.png Binary files differnew file mode 100644 index 0000000000..a001362b04 --- /dev/null +++ b/widget/cocoa/cursors/vtIBeam@2x.png diff --git a/widget/cocoa/cursors/zoomIn.png b/widget/cocoa/cursors/zoomIn.png Binary files differnew file mode 100644 index 0000000000..7ddfd4056d --- /dev/null +++ b/widget/cocoa/cursors/zoomIn.png diff --git a/widget/cocoa/cursors/zoomIn@2x.png b/widget/cocoa/cursors/zoomIn@2x.png Binary files differnew file mode 100644 index 0000000000..b1b844fa8c --- /dev/null +++ b/widget/cocoa/cursors/zoomIn@2x.png diff --git a/widget/cocoa/cursors/zoomOut.png b/widget/cocoa/cursors/zoomOut.png Binary files differnew file mode 100644 index 0000000000..ebacf25889 --- /dev/null +++ b/widget/cocoa/cursors/zoomOut.png diff --git a/widget/cocoa/cursors/zoomOut@2x.png b/widget/cocoa/cursors/zoomOut@2x.png Binary files differnew file mode 100644 index 0000000000..5f84b767ec --- /dev/null +++ b/widget/cocoa/cursors/zoomOut@2x.png 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..8e54a2c1bf --- /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.html#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.html#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.html#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..31fea3014c --- /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 uses the SDK from Xcode.app by +default, and 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 11.3 SDK (last updated in [bug 1788854](https://bugzilla.mozilla.org/show_bug.cgi?id=1788854)). 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.html#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/moz.build b/widget/cocoa/moz.build new file mode 100644 index 0000000000..ac541cc4c5 --- /dev/null +++ b/widget/cocoa/moz.build @@ -0,0 +1,184 @@ +# -*- 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" + +REQUIRES_UNIFIED_BUILD = True 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..87561a1c7d --- /dev/null +++ b/widget/cocoa/nsAppShell.mm @@ -0,0 +1,1075 @@ +/* -*- 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 "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 "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; +@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; +} + +// 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()) { + 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; +} + +@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..57a79e4ab6 --- /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, + nsWidgetInitData* aInitData = 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..b21b2d05e3 --- /dev/null +++ b/widget/cocoa/nsChildView.mm @@ -0,0 +1,4921 @@ +/* -*- 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 "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, nsWidgetInitData* 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; + + // Make sure that no composition is in progress while disconnecting + // ourselves from the view. + MutexAutoLock lock(mCompositingLock); + + if (mOnDestroyCalled) return; + mOnDestroyCalled = true; + + // Stuff below may delete the last ref to this + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + + [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: + case NS_NATIVE_DISPLAY: + 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->WindowType() == eWindowType_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->WindowType() == eWindowType_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) { + RefPtr<gfxContext> targetContext = gfxContext::CreateOrNull(aDT); + MOZ_ASSERT(targetContext); + + // 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) { + NSWindow* currentPopup = static_cast<NSWindow*>(rollupWidget->GetNativeData(NS_NATIVE_WINDOW)); + if (!nsCocoaUtils::IsEventOverWindow(theEvent, currentPopup)) { + // event is not over the rollup window, default is to roll up + bool shouldRollup = true; + + // check to see if scroll/zoom events should roll up the popup + if (isWheelTypeEvent) { + shouldRollup = rollupListener->ShouldRollupOnMouseWheelEvent(); + // consume scroll events that aren't over the popup + // unless the popup is an arrow panel + consumeEvent = rollupListener->ShouldConsumeOnMouseWheelEvent(); + } + + // 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) { + shouldRollup = false; + } else { + popupsToRollup = sameTypeCount; + } + break; + } + } + + if (shouldRollup) { + if ([theEvent type] == NSEventTypeLeftMouseDown) { + NSPoint point = [NSEvent mouseLocation]; + FlipCocoaScreenCoordinate(point); + LayoutDeviceIntPoint devPoint = mGeckoChild->CocoaPointsToDevPixels(point); + consumeEvent = (BOOL)rollupListener->Rollup(popupsToRollup, true, &devPoint, nullptr); + } else { + consumeEvent = (BOOL)rollupListener->Rollup(popupsToRollup, true, nullptr, nullptr); + } + } + } + } + + 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(kUnicodeMime); + 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->WindowType()) { + case eWindowType_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 eWindowType_toplevel: + case eWindowType_dialog: + if ([aWindow attachedSheet]) return NO; + + topLevelWindow = aWindow; + break; + case eWindowType_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..d3d34a0d55 --- /dev/null +++ b/widget/cocoa/nsClipboard.h @@ -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/. */ + +#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 SetData(nsITransferable* aTransferable, nsIClipboardOwner* anOwner, + int32_t aWhichClipboard) override; + NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard, + bool* _retval) override; + NS_IMETHOD SupportsFindClipboard(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: + // impelement the native clipboard behavior + NS_IMETHOD SetNativeClipboardData(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; + int32_t mChangeCount; // Set to the native change count after any modification of the clipboard. +}; + +#endif // nsClipboard_h_ diff --git a/widget/cocoa/nsClipboard.mm b/widget/cocoa/nsClipboard.mm new file mode 100644 index 0000000000..f1bb38873c --- /dev/null +++ b/widget/cocoa/nsClipboard.mm @@ -0,0 +1,766 @@ +/* -*- 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(), mCachedClipboard(-1), mChangeCount(0) {} + +nsClipboard::~nsClipboard() { ClearSelectionCache(); } + +NS_IMPL_ISUPPORTS_INHERITED0(nsClipboard, nsBaseClipboard) + +// 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; +} + +void nsClipboard::SetSelectionCache(nsITransferable* aTransferable) { + sSelectionCache = aTransferable; +} + +void nsClipboard::ClearSelectionCache() { sSelectionCache = nullptr; } + +NS_IMETHODIMP +nsClipboard::SetNativeClipboardData(int32_t aWhichClipboard) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard) || !mTransferable) + return NS_ERROR_FAILURE; + + NSDictionary* pasteboardOutputDict = PasteboardDictFromTransferable(mTransferable); + if (!pasteboardOutputDict) return NS_ERROR_FAILURE; + + unsigned int outputCount = [pasteboardOutputDict count]; + NSArray* outputKeys = [pasteboardOutputDict allKeys]; + NSPasteboard* cocoaPasteboard; + if (aWhichClipboard == kFindClipboard) { + cocoaPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard]; + NSString* stringType = [UTIHelper stringFromPboardType:NSPasteboardTypeString]; + [cocoaPasteboard declareTypes:[NSArray arrayWithObject:stringType] owner:nil]; + } else { + // Write everything else out to the general pasteboard. + cocoaPasteboard = [NSPasteboard generalPasteboard]; + [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 { + [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; + if ([pboardType isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeRTF]]) { + 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(flavorStr, &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; + if (aWhichClipboard == kFindClipboard) { + cocoaPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard]; + } else { + cocoaPasteboard = [NSPasteboard generalPasteboard]; + } + 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 && mChangeCount == [cocoaPasteboard changeCount]) { + if (mTransferable) { + for (uint32_t i = 0; i < flavors.Length(); i++) { + nsCString& flavorStr = flavors[i]; + + nsCOMPtr<nsISupports> dataSupports; + rv = mTransferable->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. + mEmptyingForSetData = true; + EmptyClipboard(aWhichClipboard); + mEmptyingForSetData = false; + } + + // 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; + + if (aWhichClipboard != kGlobalClipboard) return NS_OK; + + // first see if we have data for this in our cached transferable + if (mTransferable) { + nsTArray<nsCString> flavors; + nsresult rv = mTransferable->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; + } + } + } + } + } + + NSPasteboard* generalPBoard = [NSPasteboard generalPasteboard]; + + if (CLIPBOARD_LOG_ENABLED()) { + NSArray* types = [generalPBoard 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 = + [generalPBoard 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 = [generalPBoard + 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 = [generalPBoard + 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 = [generalPBoard 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); +} + +NS_IMETHODIMP +nsClipboard::SupportsFindClipboard(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = true; + return NS_OK; +} + +// 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]; + NSString* fileUTType = [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]; + if (!url || ![url absoluteString]) { + continue; + } + [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(kUnicodeMime)) { + *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; +} + +/** + * Sets the transferable object + * + */ +NS_IMETHODIMP +nsClipboard::SetData(nsITransferable* aTransferable, nsIClipboardOwner* anOwner, + int32_t aWhichClipboard) { + NS_ASSERTION(aTransferable, "clipboard given a null transferable"); + + if (aWhichClipboard == kSelectionCache) { + if (aTransferable) { + SetSelectionCache(aTransferable); + return NS_OK; + } + return NS_ERROR_FAILURE; + } + + return nsBaseClipboard::SetData(aTransferable, anOwner, aWhichClipboard); +} + +NS_IMETHODIMP +nsClipboard::EmptyClipboard(int32_t aWhichClipboard) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (aWhichClipboard == kSelectionCache) { + ClearSelectionCache(); + return NS_OK; + } + + if (!mEmptyingForSetData) { + NSPasteboard* cocoaPasteboard = nullptr; + if (aWhichClipboard == kFindClipboard) { + cocoaPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard]; + } else if (aWhichClipboard == kGlobalClipboard) { + cocoaPasteboard = [NSPasteboard generalPasteboard]; + } + if (cocoaPasteboard) { + [cocoaPasteboard clearContents]; + mChangeCount = [cocoaPasteboard changeCount]; + } + } + + 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..8cd2502112 --- /dev/null +++ b/widget/cocoa/nsCocoaUtils.h @@ -0,0 +1,557 @@ +/* -*- 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(); + + 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); + + /** + * 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..4edcc5358a --- /dev/null +++ b/widget/cocoa/nsCocoaUtils.mm @@ -0,0 +1,1768 @@ +/* -*- 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/Logging.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/Preferences.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; +} + +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 = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + ::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; + } + + RefPtr<gfxContext> context = gfxContext::CreateOrNull(drawTarget); + MOZ_ASSERT(context); + + 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(kUnicodeMime)) { + 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; + if (aFlavor.EqualsLiteral(kRTFMime)) { + 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(aFlavor, &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..887f9023a2 --- /dev/null +++ b/widget/cocoa/nsCocoaWindow.h @@ -0,0 +1,442 @@ +/* -*- 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> + +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, + nsWidgetInitData* aInitData = nullptr) override; + + [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData = 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 LayoutDeviceIntSize ClientToWindowSize(const LayoutDeviceIntSize& aClientSize) override; + + virtual void* GetNativeData(uint32_t aDataType) override; + + virtual void ConstrainPosition(bool aAllowSlop, int32_t* aX, int32_t* aY) 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; + + void WillEnterFullScreen(bool aFullScreen); + void EnteredFullScreen(bool aFullScreen, bool aNativeMode = true); + 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 nsTransparencyMode GetTransparencyMode() override; + virtual void SetTransparencyMode(nsTransparencyMode 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(LayoutDeviceIntMargin& aMargins) 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; + + protected: + virtual ~nsCocoaWindow(); + + nsresult CreateNativeWindow(const NSRect& aRect, nsBorderStyle aBorderStyle, + bool aRectIsFrameRect); + nsresult CreatePopupContentView(const LayoutDeviceIntRect& aRect, nsWidgetInitData* aInitData); + void DestroyNativeWindow(); + void UpdateBounds(); + int32_t GetWorkspaceID(); + + void DoResize(double aX, double aY, double aWidth, double aHeight, bool aRepaint, + bool aConstrainToCurrentScreen); + + inline bool ShouldToggleNativeFullscreen(bool aFullScreen, bool aUseSystemTransition); + 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; + bool mInFullScreenTransition; // true from the request to enter/exit fullscreen + // (MakeFullScreen() call) to EnteredFullScreen() + + // 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; + + bool mModal; + bool mFakeModal; + + // Whether we are currently using native fullscreen. It could be false because + // we are in the DOM fullscreen where we do not use the native fullscreen. + bool mInNativeFullScreenMode; + + 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..987376ec68 --- /dev/null +++ b/widget/cocoa/nsCocoaWindow.mm @@ -0,0 +1,4077 @@ +/* -*- 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 <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 == eWindowType_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* rollupListener = nsBaseWidget::GetActiveRollupListener(); + NS_ENSURE_TRUE_VOID(rollupListener); + + if (rollupListener->RollupNativeMenu()) { + return; + } + + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (!rollupWidget) return; + rollupListener->Rollup(0, true, nullptr, nullptr); +} + +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), + mInFullScreenTransition(false), + mIgnoreOcclusionCount(0), + mModal(false), + mFakeModal(false), + mInNativeFullScreenMode(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 'eWindowType_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;) { + nsWindowType kidType = kid->WindowType(); + if (kidType == eWindowType_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->WindowType() == eWindowType_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, nsWidgetInitData* 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 = eWindowType_toplevel; + mBorderStyle = eBorderStyle_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); + NS_ENSURE_SUCCESS(rv, rv); + + if (mWindowType == eWindowType_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, nsWidgetInitData* aInitData) { + DesktopIntRect desktopRect = + RoundedToInt(aRect / ParentBackingScaleFactor(aParent, (NSView*)aNativeParent)); + return Create(aParent, aNativeParent, desktopRect, aInitData); +} + +static unsigned int WindowMaskForBorderStyle(nsBorderStyle aBorderStyle) { + bool allOrDefault = (aBorderStyle == eBorderStyle_all || aBorderStyle == eBorderStyle_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 & eBorderStyle_title)) { + if (aBorderStyle & eBorderStyle_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 & eBorderStyle_close) { + mask |= NSWindowStyleMaskClosable; + } + if (allOrDefault || aBorderStyle & eBorderStyle_minimize) { + mask |= NSWindowStyleMaskMiniaturizable; + } + if (allOrDefault || aBorderStyle & eBorderStyle_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, nsBorderStyle aBorderStyle, + bool aRectIsFrameRect) { + 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 eWindowType_invisible: + case eWindowType_child: + break; + case eWindowType_popup: + if (aBorderStyle != eBorderStyle_default && mBorderStyle & eBorderStyle_title) { + features |= NSWindowStyleMaskTitled; + if (aBorderStyle & eBorderStyle_close) { + features |= NSWindowStyleMaskClosable; + } + } + break; + case eWindowType_toplevel: + case eWindowType_dialog: + features = WindowMaskForBorderStyle(aBorderStyle); + break; + case eWindowType_sheet: + if (mParent->WindowType() != eWindowType_invisible && aBorderStyle & eBorderStyle_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 != eWindowType_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 == eWindowType_toplevel || mWindowType == eWindowType_dialog) && + (features & NSWindowStyleMaskTitled)) + windowClass = [ToolbarWindow class]; + // If we're a popup window we need to use the PopupWindow class. + else if (mWindowType == eWindowType_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:NO]; + [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 == eWindowType_invisible) { + [mWindow setLevel:kCGDesktopWindowLevelKey]; + } + + if (mWindowType == eWindowType_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, + nsWidgetInitData* 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 != eWindowType_sheet) return nullptr; + nsCocoaWindow* parent = static_cast<nsCocoaWindow*>(mParent); + while (parent && (parent->mWindowType == eWindowType_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: + case NS_NATIVE_DISPLAY: + 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 != eWindowType_sheet) { + while (ancestor) { + if (ancestor->mNumModalDescendents++ == 0) { + NSWindow* aWindow = ancestor->GetCocoaWindow(); + if (ancestor->mWindowType != eWindowType_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 != eWindowType_sheet) { + while (ancestor) { + if (--ancestor->mNumModalDescendents == 0) { + NSWindow* aWindow = ancestor->GetCocoaWindow(); + if (ancestor->mWindowType != eWindowType_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 == eWindowType_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 == eWindowType_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 == eWindowType_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 == eWindowType_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 == ePopupLevelParent) + [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove]; + } else { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + if (mWindowType == eWindowType_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 == eWindowType_toplevel || mWindowType == eWindowType_dialog) RollUpPopups(); + + // now get rid of the window/sheet + if (mWindowType == eWindowType_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 == eWindowType_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 == eWindowType_popup) && mWasShown && ([[NSScreen screens] count] > 1); +} + +WindowRenderer* nsCocoaWindow::GetWindowRenderer() { + if (mPopupContentView) { + return mPopupContentView->GetWindowRenderer(); + } + return nullptr; +} + +nsTransparencyMode nsCocoaWindow::GetTransparencyMode() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + return (!mWindow || [mWindow isOpaque]) ? eTransparencyOpaque : eTransparencyTransparent; + + NS_OBJC_END_TRY_BLOCK_RETURN(eTransparencyOpaque); +} + +// This is called from nsMenuPopupFrame when making a popup transparent. +void nsCocoaWindow::SetTransparencyMode(nsTransparencyMode aMode) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // Only respect calls for popup windows. + if (!mWindow || mWindowType != eWindowType_popup) { + return; + } + + BOOL isTransparent = aMode == eTransparencyTransparent; + 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; } + +#define kWindowPositionSlop 20 + +void nsCocoaWindow::ConstrainPosition(bool aAllowSlop, int32_t* aX, int32_t* aY) { + 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(*aX, *aY, width, height, getter_AddRefs(screen)); + + if (screen) { + screen->GetRectDisplayPix(&(screenBounds.x), &(screenBounds.y), &(screenBounds.width), + &(screenBounds.height)); + } + } + + if (aAllowSlop) { + if (*aX < screenBounds.x - width + kWindowPositionSlop) { + *aX = screenBounds.x - width + kWindowPositionSlop; + } else if (*aX >= screenBounds.x + screenBounds.width - kWindowPositionSlop) { + *aX = screenBounds.x + screenBounds.width - kWindowPositionSlop; + } + + if (*aY < screenBounds.y - height + kWindowPositionSlop) { + *aY = screenBounds.y - height + kWindowPositionSlop; + } else if (*aY >= screenBounds.y + screenBounds.height - kWindowPositionSlop) { + *aY = screenBounds.y + screenBounds.height - kWindowPositionSlop; + } + } else { + if (*aX < screenBounds.x) { + *aX = screenBounds.x; + } else if (*aX >= screenBounds.x + screenBounds.width - width) { + *aX = screenBounds.x + screenBounds.width - width; + } + + if (*aY < screenBounds.y) { + *aY = screenBounds.y; + } else if (*aY >= screenBounds.y + screenBounds.height - height) { + *aY = 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 == eWindowType_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) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + if (!mWindow) return; + + // mSizeMode will be updated in DispatchSizeModeEvent, which will be called + // from a delegate method that handles the state change during one of the + // calls below. + nsSizeMode previousMode = mSizeMode; + + if (aMode == nsSizeMode_Normal) { + if ([mWindow isMiniaturized]) + [mWindow deminiaturize:nil]; + else if (previousMode == nsSizeMode_Maximized && [mWindow isZoomed]) + [mWindow zoom:nil]; + else if (previousMode == nsSizeMode_Fullscreen) + MakeFullScreen(false); + } else if (aMode == nsSizeMode_Minimized) { + if (![mWindow isMiniaturized]) [mWindow miniaturize:nil]; + } else if (aMode == nsSizeMode_Maximized) { + if ([mWindow isMiniaturized]) [mWindow deminiaturize:nil]; + if (![mWindow isZoomed]) [mWindow zoom:nil]; + } else if (aMode == nsSizeMode_Fullscreen) { + if (!mInFullScreenMode) MakeFullScreen(true); + } + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +// 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 != eWindowType_toplevel && mWindowType != eWindowType_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 ? eBorderStyle_none : mBorderStyle, true); + 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::WillEnterFullScreen(bool aFullScreen) { + if (mWidgetListener) { + mWidgetListener->FullscreenWillChange(aFullScreen); + } + // Update the state to full screen when we are entering, so that we switch to + // full screen view as soon as possible. + UpdateFullscreenState(aFullScreen, true); +} + +void nsCocoaWindow::EnteredFullScreen(bool aFullScreen, bool aNativeMode) { + mInFullScreenTransition = false; + UpdateFullscreenState(aFullScreen, aNativeMode); +} + +void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) { + bool wasInFullscreen = mInFullScreenMode; + mInFullScreenMode = aFullScreen; + if (aNativeMode || mInNativeFullScreenMode) { + mInNativeFullScreenMode = aFullScreen; + } + DispatchSizeModeEvent(); + if (mWidgetListener && wasInFullscreen != aFullScreen) { + mWidgetListener->FullscreenChanged(aFullScreen); + } + + // Notify the mainChildView with our new fullscreen state. + nsChildView* mainChildView = static_cast<nsChildView*>([[mWindow mainChildView] widget]); + if (mainChildView) { + mainChildView->UpdateFullscreen(aFullScreen); + } +} + +inline bool nsCocoaWindow::ShouldToggleNativeFullscreen(bool aFullScreen, + bool aUseSystemTransition) { + // First check if this window supports entering native fullscreen. + // This is set based on the macnativefullscreen attribute on the window's + // document element. + NSWindowCollectionBehavior colBehavior = [mWindow collectionBehavior]; + if (!(colBehavior & NSWindowCollectionBehaviorFullScreenPrimary)) { + return false; + } + + if (mInNativeFullScreenMode) { + // If we are using native fullscreen, go ahead to exit it. + return true; + } + if (!aUseSystemTransition) { + // If we do not want the system fullscreen transition, + // don't use the native fullscreen. + return false; + } + // If we are using native fullscreen, we should have returned earlier. + return 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) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (!mWindow) { + return NS_OK; + } + + // We will call into MakeFullScreen redundantly when entering/exiting + // fullscreen mode via OS X controls. When that happens we should just handle + // it gracefully - no need to ASSERT. + if (mInFullScreenMode == aFullScreen) { + return NS_OK; + } + + mInFullScreenTransition = true; + + if (ShouldToggleNativeFullscreen(aFullScreen, aUseSystemTransition)) { + MOZ_ASSERT(mInNativeFullScreenMode != aFullScreen, + "We shouldn't have been in native fullscreen."); + // Calling toggleFullScreen will result in windowDid(FailTo)?(Enter|Exit)FullScreen + // to be called from the OS. We will call EnteredFullScreen from those methods, + // where mInFullScreenMode will be set and a sizemode event will be dispatched. + [mWindow toggleFullScreen:nil]; + } else { + if (mWidgetListener) { + mWidgetListener->FullscreenWillChange(aFullScreen); + } + NSDisableScreenUpdates(); + // 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(aFullScreen); + nsBaseWidget::InfallibleMakeFullScreen(aFullScreen); + NSEnableScreenUpdates(); + EnteredFullScreen(aFullScreen, /* aNativeMode */ false); + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +// 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->WindowType() == eWindowType_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 == eWindowType_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; + } + + nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode); + + // Don't dispatch a sizemode event if: + // 1. the window is transitioning to fullscreen + // 2. the new sizemode is the same as the current sizemode + if (mInFullScreenTransition || 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; + } + + bool newOcclusionState = !([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)); +} + +LayoutDeviceIntSize nsCocoaWindow::ClientToWindowSize(const LayoutDeviceIntSize& aClientSize) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (!mWindow) return LayoutDeviceIntSize(0, 0); + + CGFloat backingScale = BackingScaleFactor(); + LayoutDeviceIntRect r(0, 0, aClientSize.width, aClientSize.height); + NSRect rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, backingScale); + + // Our caller expects the inflated rect for windows *with separate titlebars*, + // i.e. for windows where [mWindow drawsContentsIntoWindowFrame] is NO. + // + // So we call frameRectForContentRect on NSWindow here, instead of mWindow, so + // that we don't run into our override if this window is a window that draws + // its content into the titlebar. + // + // This is the same thing the windows widget does, but we probably should fix + // that, see bug 1445738. + NSUInteger styleMask = [mWindow styleMask]; + styleMask &= ~NSWindowStyleMaskFullSizeContentView; + NSRect inflatedRect = [NSWindow frameRectForContentRect:rect styleMask:styleMask]; + r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(inflatedRect, backingScale); + return r.Size(); + + NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntSize(0, 0)); +} + +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 == eWindowType_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 == eWindowType_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 != eWindowType_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 == eWindowType_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(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 == ePopupLevelFloating) { + [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; +} + +- (void)windowDidResize:(NSNotification*)aNotification { + BaseWindow* window = [aNotification object]; + [window updateTrackingArea]; + + if (!mGeckoWindow) return; + + // Resizing might have changed our zoom state. + mGeckoWindow->DispatchSizeModeEvent(); + mGeckoWindow->ReportSizeEvent(); +} + +- (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->WillEnterFullScreen(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 { + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(true); + + // 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]; + } +} + +- (void)windowWillExitFullScreen:(NSNotification*)notification { + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->WillEnterFullScreen(false); +} + +- (void)windowDidExitFullScreen:(NSNotification*)notification { + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(false); +} + +- (void)windowDidFailToEnterFullScreen:(NSWindow*)window { + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(false); +} + +- (void)windowDidFailToExitFullScreen:(NSWindow*)window { + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(true); +} + +- (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(); + 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) mGeckoWindow->DispatchSizeModeEvent(); +} + +- (void)windowDidDeminiaturize:(NSNotification*)aNotification { + if (mGeckoWindow) mGeckoWindow->DispatchSizeModeEvent(); +} + +- (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) { + nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); + if (listener) { + listener->WindowActivated(); + } + mToplevelActiveState = true; + } +} + +- (void)sendToplevelDeactivateEvents { + if (mToplevelActiveState && mGeckoWindow) { + nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); + if (listener) { + listener->WindowDeactivated(); + } + 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..16f7dcb916 --- /dev/null +++ b/widget/cocoa/nsCursorManager.mm @@ -0,0 +1,314 @@ +/* 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) { + SEL cursorSelector; + 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: + cursorSelector = @selector(dragCopyCursor); + return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector] + ? [NSCursor performSelector:cursorSelector] + : [NSCursor arrowCursor] + type:aCursor]; + case eCursor_alias: + cursorSelector = @selector(dragLinkCursor); + return [nsMacCursor cursorWithCursor:[NSCursor respondsToSelector:cursorSelector] + ? [NSCursor performSelector:cursorSelector] + : [NSCursor arrowCursor] + type:aCursor]; + case eCursor_context_menu: + 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: + 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, ¤tPrinter); + 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..ea9af44856 --- /dev/null +++ b/widget/cocoa/nsDragService.mm @@ -0,0 +1,476 @@ +/* -*- 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 = YES; + + 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(kUnicodeMime)) { + 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..2b1c60bdae --- /dev/null +++ b/widget/cocoa/nsFilePicker.h @@ -0,0 +1,72 @@ +/* -*- 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(); + + 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(int16_t* _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. + int16_t GetLocalFiles(bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles); + int16_t GetLocalFolder(nsIFile** outFile); + int16_t 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..79abb6014c --- /dev/null +++ b/widget/cocoa/nsFilePicker.mm @@ -0,0 +1,638 @@ +/* -*- 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(int16_t* retval) { + NS_ENSURE_ARG_POINTER(retval); + + *retval = returnCancel; + + int16_t 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. +int16_t nsFilePicker::GetLocalFiles(bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + int16_t retVal = (int16_t)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(0); +} + +// Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in the dialog. +int16_t nsFilePicker::GetLocalFolder(nsIFile** outFile) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer"); + + int16_t retVal = (int16_t)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(0); +} + +// Returns |returnOK| if the user presses OK in the dialog. +int16_t nsFilePicker::PutLocalFile(nsIFile** outFile) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer"); + + int16_t retVal = 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(0); +} + +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..5967a51a84 --- /dev/null +++ b/widget/cocoa/nsLookAndFeel.mm @@ -0,0 +1,685 @@ +/* -*- 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::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..b2f8a64f60 --- /dev/null +++ b/widget/cocoa/nsMacUserActivityUpdater.mm @@ -0,0 +1,63 @@ +/* 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" + +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..0ecc0117ce --- /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 "nsMenuGroupOwnerX.h" +#include "nsChangeObserver.h" +#include "nsTArray.h" +#include "nsString.h" + +class nsMenuBarX; +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..0217fe4a8d --- /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 "nsTHashMap.h" +#include "nsString.h" + +class nsMenuBarX; +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..345a0478e2 --- /dev/null +++ b/widget/cocoa/nsMenuGroupOwnerX.mm @@ -0,0 +1,228 @@ +/* -*- 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::NativeAnonymousChildListChange(nsIContent* aContent, bool aIsRemove) {} + +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..c2d3442cbb --- /dev/null +++ b/widget/cocoa/nsMenuItemIconX.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 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] + nsIntRect mImageRegionRect; + 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..4beb04aef1 --- /dev/null +++ b/widget/cocoa/nsMenuItemIconX.mm @@ -0,0 +1,189 @@ +/* -*- 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; + } + mImageRegionRect.SetEmpty(); + 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; + } + + // Check if the icon has a specified image region so that it can be + // cropped appropriately before being displayed. + const nsRect r = sc->StyleList()->GetImageRegion(); + + // Return nullptr if the image region is invalid so the image + // is not drawn, and behavior is similar to XUL menus. + if (r.X() < 0 || r.Y() < 0 || r.Width() < 0 || r.Height() < 0) { + return nullptr; + } + + // 'auto' is represented by a [0, 0, 0, 0] rect. Only set mImageRegionRect + // if we have some other value. + if (r.IsEmpty()) { + mImageRegionRect.SetEmpty(); + } else { + mImageRegionRect = r.ToNearestPixels(mozilla::AppUnitsPerCSSPixel()); + } + 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 + subrect:mImageRegionRect + 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..3cbde81c62 --- /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::Event; +using mozilla::dom::CallerType; + +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..f744701051 --- /dev/null +++ b/widget/cocoa/nsMenuX.mm @@ -0,0 +1,1365 @@ +/* -*- 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 "plstr.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)); + + 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; + + // Disable the Services item for now. Bug 660452 tracks turning this on for the appropriate menus. + myMenu.allowsContextMenuPlugIns = 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); + } +} + +// 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, true, nullptr, nullptr); + [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 diff --git a/widget/cocoa/nsNativeThemeCocoa.h b/widget/cocoa/nsNativeThemeCocoa.h new file mode 100644 index 0000000000..b7557860d9 --- /dev/null +++ b/widget/cocoa/nsNativeThemeCocoa.h @@ -0,0 +1,425 @@ +/* -*- 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 + eFocusOutline, + 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, + eResizer + }; + + 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 FocusOutline() { return WidgetInfo(Widget::eFocusOutline, false); } + 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); } + static WidgetInfo Resizer(bool aParams) { return WidgetInfo(Widget::eResizer, 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, NSCellStateValue 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 DrawFocusOutline(CGContextRef cgContext, const HIRect& inBoxRect); + 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 DrawResizer(CGContextRef cgContext, const HIRect& aRect, bool aIsRTL); + 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..c31e5af463 --- /dev/null +++ b/widget/cocoa/nsNativeThemeCocoa.mm @@ -0,0 +1,3493 @@ +/* -*- 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); +} + +// 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 = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + 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 = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + + 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->WindowType() == eWindowType_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 graphicsContextWithGraphicsPort: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 graphicsContextWithGraphicsPort: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 graphicsContextWithGraphicsPort: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 NSCellStateValue 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 = + aParams.disabled ? @"disabled" : (aParams.insideActiveMenuItem ? @"pressed" : @"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; + // 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); + 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, + NSCellStateValue 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; +} + +void nsNativeThemeCocoa::DrawFocusOutline(CGContextRef cgContext, const HIRect& inBoxRect) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext + flipped:YES]]; + CGContextSaveGState(cgContext); + NSSetFocusRingStyle(NSFocusRingOnly); + NSRectFill(NSRectFromCGRect(inBoxRect)); + CGContextRestoreGState(cgContext); + [NSGraphicsContext setCurrentContext:savedContext]; + + 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; +} + +static void RenderResizer(CGContextRef cgContext, const HIRect& aRenderRect, void* aData) { + HIThemeGrowBoxDrawInfo* drawInfo = (HIThemeGrowBoxDrawInfo*)aData; + HIThemeDrawGrowBox(&CGPointZero, drawInfo, cgContext, kHIThemeOrientationNormal); +} + +void nsNativeThemeCocoa::DrawResizer(CGContextRef cgContext, const HIRect& aRect, bool aIsRTL) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeGrowBoxDrawInfo drawInfo; + drawInfo.version = 0; + drawInfo.state = kThemeStateActive; + drawInfo.kind = kHIThemeGrowBoxKindNormal; + drawInfo.direction = kThemeGrowRight | kThemeGrowDown; + drawInfo.size = kHIThemeGrowBoxSizeNormal; + + RenderTransformedHIThemeControl(cgContext, aRect, RenderResizer, &drawInfo, aIsRTL); + + 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::FocusOutline: + return Some(WidgetInfo::FocusOutline()); + + 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))); + + case StyleAppearance::Resizer: + return Some(WidgetInfo::Resizer(IsFrameRTL(aFrame))); + + default: + break; + } + + return Nothing(); + + NS_OBJC_END_TRY_BLOCK_RETURN(Nothing()); +} + +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 (IsWidgetScrollbarPart(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::eFocusOutline: { + DrawFocusOutline(cgContext, macRect); + 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; + } + case Widget::eResizer: { + bool isRTL = aWidgetInfo.Params<bool>(); + DrawResizer(cgContext, macRect, isRTL); + 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 (IsWidgetScrollbarPart(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::FocusOutline: + 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: + case StyleAppearance::Resizer: + 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) { + 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: + case StyleAppearance::FocusOutline: { + 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 (IsWidgetScrollbarPart(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); + + case StyleAppearance::Resizer: { + HIThemeGrowBoxDrawInfo drawInfo; + drawInfo.version = 0; + drawInfo.state = kThemeStateActive; + drawInfo.kind = kHIThemeGrowBoxKindNormal; + drawInfo.direction = kThemeGrowRight | kThemeGrowDown; + drawInfo.size = kHIThemeGrowBoxSizeNormal; + HIPoint pnt = {0, 0}; + HIRect bounds; + HIThemeGetGrowBoxBounds(&pnt, &drawInfo, &bounds); + result.SizeTo(bounds.size.width, bounds.size.height); + break; + } + 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::Statusbarpanel: + case StyleAppearance::Resizerpanel: + 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 (IsWidgetScrollbarPart(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); + + case StyleAppearance::Resizer: { + nsIFrame* parentFrame = aFrame->GetParent(); + if (!parentFrame || !parentFrame->IsScrollFrame()) return true; + + // Note that IsWidgetStyled is not called for resizers on Mac. This is + // because for scrollable containers, the native resizer looks better + // when (non-overlay) scrollbars are present even when the style is + // overriden, and the custom transparent resizer looks better when + // scrollbars are not present. + nsIScrollableFrame* scrollFrame = do_QueryFrame(parentFrame); + return (!LookAndFeel::UseOverlayScrollbars() && scrollFrame && + (!scrollFrame->GetScrollbarVisibility().isEmpty())); + } + + case StyleAppearance::FocusOutline: + return true; + + 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: + case StyleAppearance::Resizer: + 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..5272bc0f20 --- /dev/null +++ b/widget/cocoa/nsPrintSettingsX.mm @@ -0,0 +1,360 @@ +/* -*- 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 "plstr.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) { + [printInfo setOrientation:NSPaperOrientationPortrait]; + paperSize.width = CocoaPointsFromPaperSize(mPaperWidth); + paperSize.height = CocoaPointsFromPaperSize(mPaperHeight); + [printInfo setPaperSize:paperSize]; + } else { + [printInfo setOrientation:NSPaperOrientationLandscape]; + paperSize.width = CocoaPointsFromPaperSize(mPaperHeight); + paperSize.height = CocoaPointsFromPaperSize(mPaperWidth); + [printInfo setPaperSize:paperSize]; + } + + [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..3125d9da0f --- /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, ¬ifyPortRef, 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(0, false, nullptr, nullptr); + }]; + } + + 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..98c2344395 --- /dev/null +++ b/widget/cocoa/nsTouchBar.mm @@ -0,0 +1,604 @@ +/* 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 "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..92f9ab0302 --- /dev/null +++ b/widget/cocoa/nsTouchBarInputIcon.h @@ -0,0 +1,72 @@ +/* -*- 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; + nsIntRect mImageRegionRect; + 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..a99002c505 --- /dev/null +++ b/widget/cocoa/nsTouchBarInputIcon.mm @@ -0,0 +1,134 @@ +/* -*- 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 + subrect:mImageRegionRect + 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..43efbb2591 --- /dev/null +++ b/widget/cocoa/nsUserIdleServiceX.h @@ -0,0 +1,31 @@ +/* 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() {} + bool UsePollMode() override; +}; + +#endif // nsUserIdleServiceX_h_ diff --git a/widget/cocoa/nsUserIdleServiceX.mm b/widget/cocoa/nsUserIdleServiceX.mm new file mode 100644 index 0000000000..a1b8dd1c3c --- /dev/null +++ b/widget/cocoa/nsUserIdleServiceX.mm @@ -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/. */ + +#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); +} + +bool nsUserIdleServiceX::UsePollMode() { return true; } 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..2f6442400e --- /dev/null +++ b/widget/cocoa/nsWindowMap.mm @@ -0,0 +1,280 @@ +/* -*- 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]) { + [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]) { + [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 open above it -- + // as far as Gecko is concerned, it's inactive, and stays so until the sheet + // closes. + if (delegate && [delegate isKindOfClass:[WindowDelegate class]] && ![window attachedSheet]) + [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 Binary files differnew file mode 100644 index 0000000000..16b3f7e523 --- /dev/null +++ b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib |