diff options
Diffstat (limited to 'widget/cocoa')
149 files changed, 39986 insertions, 0 deletions
diff --git a/widget/cocoa/ComplexTextInputPanel.h b/widget/cocoa/ComplexTextInputPanel.h new file mode 100644 index 000000000..648e6d911 --- /dev/null +++ b/widget/cocoa/ComplexTextInputPanel.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Modified by Josh Aas of Mozilla Corporation. + */ + +#ifndef ComplexTextInputPanel_h_ +#define ComplexTextInputPanel_h_ + +#include "nsString.h" +#include "npapi.h" + +class ComplexTextInputPanel +{ +public: + static ComplexTextInputPanel* GetSharedComplexTextInputPanel(); + virtual void PlacePanel(int32_t x, int32_t y) = 0; // Bottom left coordinate of plugin in screen coords + virtual void InterpretKeyEvent(void* aEvent, nsAString& aOutText) = 0; + virtual bool IsInComposition() = 0; + virtual void* GetInputContext() = 0; + virtual void CancelComposition() = 0; + +protected: + virtual ~ComplexTextInputPanel() {}; +}; + +#endif // ComplexTextInputPanel_h_ diff --git a/widget/cocoa/ComplexTextInputPanel.mm b/widget/cocoa/ComplexTextInputPanel.mm new file mode 100644 index 000000000..a4b58955e --- /dev/null +++ b/widget/cocoa/ComplexTextInputPanel.mm @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Modified by Josh Aas of Mozilla Corporation. + */ + +#import "ComplexTextInputPanel.h" + +#import <Cocoa/Cocoa.h> + +#include <algorithm> +#include "mozilla/Preferences.h" +#include "nsChildView.h" + +using namespace mozilla; + +extern "C" OSStatus TSMProcessRawKeyEvent(EventRef anEvent); + +#define kInputWindowHeight 20 + +@interface ComplexTextInputPanelImpl : NSPanel { + NSTextView *mInputTextView; +} + ++ (ComplexTextInputPanelImpl*)sharedComplexTextInputPanelImpl; + +- (NSTextInputContext*)inputContext; +- (void)interpretKeyEvent:(NSEvent*)event string:(NSString**)string; +- (void)cancelComposition; +- (BOOL)inComposition; + +// This places the text input panel fully onscreen and below the lower left +// corner of the focused plugin. +- (void)adjustTo:(NSPoint)point; + +@end + +@implementation ComplexTextInputPanelImpl + ++ (ComplexTextInputPanelImpl*)sharedComplexTextInputPanelImpl +{ + static ComplexTextInputPanelImpl *sComplexTextInputPanelImpl; + if (!sComplexTextInputPanelImpl) + sComplexTextInputPanelImpl = [[ComplexTextInputPanelImpl alloc] init]; + return sComplexTextInputPanelImpl; +} + +- (id)init +{ + // In the original Apple code the style mask is given by a function which is not open source. + // What could possibly be worth hiding in that function, I do not know. + // Courtesy of gdb: stylemask: 011000011111, 0x61f + self = [super initWithContentRect:NSZeroRect styleMask:0x61f backing:NSBackingStoreBuffered defer:YES]; + if (!self) + return nil; + + // Set the frame size. + NSRect visibleFrame = [[NSScreen mainScreen] visibleFrame]; + NSRect frame = NSMakeRect(visibleFrame.origin.x, visibleFrame.origin.y, visibleFrame.size.width, kInputWindowHeight); + + [self setFrame:frame display:NO]; + + mInputTextView = [[NSTextView alloc] initWithFrame:[self.contentView frame]]; + mInputTextView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable | NSViewMaxXMargin | NSViewMinXMargin | NSViewMaxYMargin | NSViewMinYMargin; + + NSScrollView* scrollView = [[NSScrollView alloc] initWithFrame:[self.contentView frame]]; + scrollView.documentView = mInputTextView; + self.contentView = scrollView; + [scrollView release]; + + [self setFloatingPanel:YES]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardInputSourceChanged:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object:nil]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [mInputTextView release]; + + [super dealloc]; +} + +- (void)keyboardInputSourceChanged:(NSNotification *)notification +{ + static int8_t sDoCancel = -1; + if (!sDoCancel || ![self inComposition]) { + return; + } + if (sDoCancel < 0) { + bool cancelComposition = false; + static const char* kPrefName = + "ui.plugin.cancel_composition_at_input_source_changed"; + nsresult rv = Preferences::GetBool(kPrefName, &cancelComposition); + NS_ENSURE_SUCCESS(rv, ); + sDoCancel = cancelComposition ? 1 : 0; + } + if (sDoCancel) { + [self cancelComposition]; + } +} + +- (void)interpretKeyEvent:(NSEvent*)event string:(NSString**)string +{ + *string = nil; + + if (![[mInputTextView inputContext] handleEvent:event]) { + return; + } + + if ([mInputTextView hasMarkedText]) { + // Don't show the input method window for dead keys + if ([[event characters] length] > 0) { + [self orderFront:nil]; + } + return; + } else { + [self orderOut:nil]; + + NSString *text = [[mInputTextView textStorage] string]; + if ([text length] > 0) { + *string = [[text copy] autorelease]; + } + } + + [mInputTextView setString:@""]; +} + +- (NSTextInputContext*)inputContext +{ + return [mInputTextView inputContext]; +} + +- (void)cancelComposition +{ + [mInputTextView setString:@""]; + [self orderOut:nil]; +} + +- (BOOL)inComposition +{ + return [mInputTextView hasMarkedText]; +} + +- (void)adjustTo:(NSPoint)point +{ + NSRect selfRect = [self frame]; + NSRect rect = NSMakeRect(point.x, + point.y - selfRect.size.height, + 500, + selfRect.size.height); + + // Adjust to screen. + NSRect screenRect = [[NSScreen mainScreen] visibleFrame]; + if (rect.origin.x < screenRect.origin.x) { + rect.origin.x = screenRect.origin.x; + } + if (rect.origin.y < screenRect.origin.y) { + rect.origin.y = screenRect.origin.y; + } + CGFloat xMostOfScreen = screenRect.origin.x + screenRect.size.width; + CGFloat yMostOfScreen = screenRect.origin.y + screenRect.size.height; + CGFloat xMost = rect.origin.x + rect.size.width; + CGFloat yMost = rect.origin.y + rect.size.height; + if (xMostOfScreen < xMost) { + rect.origin.x -= xMost - xMostOfScreen; + } + if (yMostOfScreen < yMost) { + rect.origin.y -= yMost - yMostOfScreen; + } + + [self setFrame:rect display:[self isVisible]]; +} + +@end + +class ComplexTextInputPanelPrivate : public ComplexTextInputPanel +{ +public: + ComplexTextInputPanelPrivate(); + + virtual void InterpretKeyEvent(void* aEvent, nsAString& aOutText); + virtual bool IsInComposition(); + virtual void PlacePanel(int32_t x, int32_t y); + virtual void* GetInputContext() { return [mPanel inputContext]; } + virtual void CancelComposition() { [mPanel cancelComposition]; } + +private: + ~ComplexTextInputPanelPrivate(); + ComplexTextInputPanelImpl* mPanel; +}; + +ComplexTextInputPanelPrivate::ComplexTextInputPanelPrivate() +{ + mPanel = [[ComplexTextInputPanelImpl alloc] init]; +} + +ComplexTextInputPanelPrivate::~ComplexTextInputPanelPrivate() +{ + [mPanel release]; +} + +ComplexTextInputPanel* +ComplexTextInputPanel::GetSharedComplexTextInputPanel() +{ + static ComplexTextInputPanelPrivate *sComplexTextInputPanelPrivate; + if (!sComplexTextInputPanelPrivate) { + sComplexTextInputPanelPrivate = new ComplexTextInputPanelPrivate(); + } + return sComplexTextInputPanelPrivate; +} + +void +ComplexTextInputPanelPrivate::InterpretKeyEvent(void* aEvent, nsAString& aOutText) +{ + NSString* textString = nil; + [mPanel interpretKeyEvent:(NSEvent*)aEvent string:&textString]; + + if (textString) { + nsCocoaUtils::GetStringForNSString(textString, aOutText); + } +} + +bool +ComplexTextInputPanelPrivate::IsInComposition() +{ + return !![mPanel inComposition]; +} + +void +ComplexTextInputPanelPrivate::PlacePanel(int32_t x, int32_t y) +{ + [mPanel adjustTo:NSMakePoint(x, y)]; +} diff --git a/widget/cocoa/CustomCocoaEvents.h b/widget/cocoa/CustomCocoaEvents.h new file mode 100644 index 000000000..0043f0d69 --- /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 + * NSApplicationDefined 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/GfxInfo.h b/widget/cocoa/GfxInfo.h new file mode 100644 index 000000000..05bdad158 --- /dev/null +++ b/widget/cocoa/GfxInfo.h @@ -0,0 +1,95 @@ +/* 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 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(nsAString & aAdapterRAM) 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(nsAString & aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVersion2(nsAString & aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate2(nsAString & aAdapterDriverDate) override; + NS_IMETHOD GetIsGPU2Active(bool *aIsGPU2Active) override; + + using GfxInfoBase::GetFeatureStatus; + using GfxInfoBase::GetFeatureSuggestedDriverVersion; + using GfxInfoBase::GetWebGLParameter; + + virtual nsresult Init() override; + +#ifdef DEBUG + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIGFXINFODEBUG +#endif + + virtual uint32_t OperatingSystemVersion() override { return mOSXVersion; } + + nsresult FindMonitors(JSContext* cx, JS::HandleObject array) override; + +protected: + + virtual ~GfxInfo() {} + + 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(); + + nsString mAdapterRAMString; + nsString mDeviceID; + nsString mDriverVersion; + nsString mDriverDate; + nsString mDeviceKey; + + nsString mAdapterVendorID; + nsString mAdapterDeviceID; + + 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 000000000..6789ae8b2 --- /dev/null +++ b/widget/cocoa/GfxInfo.mm @@ -0,0 +1,433 @@ +/* -*- 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 "nsCocoaFeatures.h" +#include "mozilla/Preferences.h" +#include <algorithm> + +#import <Foundation/Foundation.h> +#import <IOKit/IOKitLib.h> +#import <Cocoa/Cocoa.h> + +#if defined(MOZ_CRASHREPORTER) +#include "nsExceptionHandler.h" +#include "nsICrashReporter.h" +#define NS_CRASHREPORTER_CONTRACTID "@mozilla.org/toolkit/crash-reporter;1" +#endif + +using namespace mozilla; +using namespace mozilla::widget; + +#ifdef DEBUG +NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug) +#endif + +GfxInfo::GfxInfo() +{ +} + +static OperatingSystem +OSXVersionToOperatingSystem(uint32_t aOSXVersion) +{ + if (nsCocoaFeatures::ExtractMajorVersion(aOSXVersion) == 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; + } + } + + 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() +{ + io_registry_entry_t dsp_port = CGDisplayIOServicePort(kCGDirectMainDisplay); + CFTypeRef vendor_id_ref = SearchPortForProperty(dsp_port, CFSTR("vendor-id")); + if (vendor_id_ref) { + mAdapterVendorID.AppendPrintf("0x%04x", IntValueOfCFData((CFDataRef)vendor_id_ref)); + CFRelease(vendor_id_ref); + } + CFTypeRef device_id_ref = SearchPortForProperty(dsp_port, CFSTR("device-id")); + if (device_id_ref) { + mAdapterDeviceID.AppendPrintf("0x%04x", IntValueOfCFData((CFDataRef)device_id_ref)); + CFRelease(device_id_ref); + } +} + +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::OSXVersion(); + + return rv; +} + +NS_IMETHODIMP +GfxInfo::GetD2DEnabled(bool *aEnabled) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetDWriteEnabled(bool *aEnabled) +{ + return NS_ERROR_FAILURE; +} + +/* readonly attribute DOMString DWriteVersion; */ +NS_IMETHODIMP +GfxInfo::GetDWriteVersion(nsAString & aDwriteVersion) +{ + return NS_ERROR_FAILURE; +} + +/* readonly attribute DOMString cleartypeParameters; */ +NS_IMETHODIMP +GfxInfo::GetCleartypeParameters(nsAString & aCleartypeParams) +{ + return NS_ERROR_FAILURE; +} + +/* 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) +{ + return NS_ERROR_FAILURE; +} + +/* readonly attribute DOMString adapterRAM; */ +NS_IMETHODIMP +GfxInfo::GetAdapterRAM(nsAString & aAdapterRAM) +{ + aAdapterRAM = mAdapterRAMString; + return NS_OK; +} + +/* readonly attribute DOMString adapterRAM2; */ +NS_IMETHODIMP +GfxInfo::GetAdapterRAM2(nsAString & aAdapterRAM) +{ + return NS_ERROR_FAILURE; +} + +/* 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) +{ + return NS_ERROR_FAILURE; +} + +/* 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) +{ + return NS_ERROR_FAILURE; +} + +/* 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) +{ + return NS_ERROR_FAILURE; +} + +/* readonly attribute DOMString adapterVendorID; */ +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID(nsAString & aAdapterVendorID) +{ + aAdapterVendorID = mAdapterVendorID; + return NS_OK; +} + +/* readonly attribute DOMString adapterVendorID2; */ +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID2(nsAString & aAdapterVendorID) +{ + return NS_ERROR_FAILURE; +} + +/* readonly attribute DOMString adapterDeviceID; */ +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID(nsAString & aAdapterDeviceID) +{ + aAdapterDeviceID = mAdapterDeviceID; + return NS_OK; +} + +/* readonly attribute DOMString adapterDeviceID2; */ +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID2(nsAString & aAdapterDeviceID) +{ + return NS_ERROR_FAILURE; +} + +/* 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; +} + +/* readonly attribute boolean isGPU2Active; */ +NS_IMETHODIMP +GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) +{ + return NS_ERROR_FAILURE; +} + +void +GfxInfo::AddCrashReportAnnotations() +{ +#if defined(MOZ_CRASHREPORTER) + 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(NS_LITERAL_CSTRING("AdapterVendorID"), + narrowVendorID); + CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("AdapterDeviceID"), + narrowDeviceID); + CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("AdapterDriverVersion"), + narrowDriverVersion); + /* Add an App Note for now so that we get the data immediately. These + * can go away after we store the above in the socorro db */ + nsAutoCString note; + /* AppendPrintf only supports 32 character strings, mrghh. */ + note.Append("AdapterVendorID: "); + note.Append(narrowVendorID); + note.Append(", AdapterDeviceID: "); + note.Append(narrowDeviceID); + CrashReporter::AppendAppNotesToCrashReport(note); +#endif +} + +// We don't support checking driver versions on Mac. +#define IMPLEMENT_MAC_DRIVER_BLOCKLIST(os, vendor, device, features, blockOn, ruleId) \ + APPEND_TO_DRIVER_BLOCKLIST(os, vendor, device, features, blockOn, \ + DRIVER_COMPARISON_IGNORED, V(0,0,0,0), ruleId, "") + + +const nsTArray<GfxDriverInfo>& +GfxInfo::GetGfxDriverInfo() +{ + if (!mDriverInfo->Length()) { + IMPLEMENT_MAC_DRIVER_BLOCKLIST(OperatingSystem::OSX, + (nsAString&) GfxDriverInfo::GetDeviceVendor(VendorATI), GfxDriverInfo::allDevices, + nsIGfxInfo::FEATURE_WEBGL_MSAA, nsIGfxInfo::FEATURE_BLOCKED_OS_VERSION, "FEATURE_FAILURE_MAC_ATI_NO_MSAA"); + IMPLEMENT_MAC_DRIVER_BLOCKLIST(OperatingSystem::OSX, + (nsAString&) GfxDriverInfo::GetDeviceVendor(VendorATI), (GfxDeviceFamily*) GfxDriverInfo::GetDeviceFamily(RadeonX1000), + nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_MAC_RADEONX1000_NO_TEXTURE2D"); + IMPLEMENT_MAC_DRIVER_BLOCKLIST(OperatingSystem::OSX, + (nsAString&) GfxDriverInfo::GetDeviceVendor(VendorNVIDIA), (GfxDeviceFamily*) GfxDriverInfo::GetDeviceFamily(Geforce7300GT), + nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, "FEATURE_FAILURE_MAC_7300_NO_WEBGL"); + } + return *mDriverInfo; +} + +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; + + // Don't evaluate special cases when we're evaluating the downloaded blocklist. + if (!aDriverInfo.Length()) { + if (aFeature == nsIGfxInfo::FEATURE_WEBGL_MSAA) { + // Blacklist all ATI cards on OSX, except for + // 0x6760 and 0x9488 + if (mAdapterVendorID.Equals(GfxDriverInfo::GetDeviceVendor(VendorATI), nsCaseInsensitiveStringComparator()) && + (mAdapterDeviceID.LowerCaseEqualsLiteral("0x6760") || + mAdapterDeviceID.LowerCaseEqualsLiteral("0x9488"))) { + *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + return NS_OK; + } + } else 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; + } + } + + return GfxInfoBase::GetFeatureStatusImpl(aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os); +} + +nsresult +GfxInfo::FindMonitors(JSContext* aCx, JS::HandleObject aOutArray) +{ + // 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. + int32_t deviceCount = 0; + for (NSScreen* screen in [NSScreen screens]) { + NSRect rect = [screen frame]; + + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + + JS::Rooted<JS::Value> screenWidth(aCx, JS::Int32Value((int)rect.size.width)); + JS_SetProperty(aCx, obj, "screenWidth", screenWidth); + + JS::Rooted<JS::Value> screenHeight(aCx, JS::Int32Value((int)rect.size.height)); + JS_SetProperty(aCx, obj, "screenHeight", screenHeight); + + JS::Rooted<JS::Value> scale(aCx, JS::NumberValue(nsCocoaUtils::GetBackingScaleFactor(screen))); + JS_SetProperty(aCx, obj, "scale", scale); + + JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj)); + JS_SetElement(aCx, aOutArray, deviceCount++, element); + } + return NS_OK; +} + +#ifdef DEBUG + +// Implement nsIGfxInfoDebug + +/* void spoofVendorID (in DOMString aVendorID); */ +NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString & aVendorID) +{ + mAdapterVendorID = aVendorID; + return NS_OK; +} + +/* void spoofDeviceID (in unsigned long aDeviceID); */ +NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString & aDeviceID) +{ + mAdapterDeviceID = aDeviceID; + return NS_OK; +} + +/* void spoofDriverVersion (in DOMString aDriverVersion); */ +NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString & aDriverVersion) +{ + mDriverVersion = aDriverVersion; + return NS_OK; +} + +/* void spoofOSVersion (in unsigned long aVersion); */ +NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) +{ + mOSXVersion = aVersion; + return NS_OK; +} + +#endif diff --git a/widget/cocoa/NativeKeyBindings.h b/widget/cocoa/NativeKeyBindings.h new file mode 100644 index 000000000..d1ba2c370 --- /dev/null +++ b/widget/cocoa/NativeKeyBindings.h @@ -0,0 +1,48 @@ +/* -*- 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_NativeKeyBindings_h_ +#define mozilla_widget_NativeKeyBindings_h_ + +#import <Cocoa/Cocoa.h> +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsDataHashtable.h" +#include "nsIWidget.h" + +namespace mozilla { +namespace widget { + +typedef nsDataHashtable<nsPtrHashKey<struct objc_selector>, CommandInt> + SelectorCommandHashtable; + +class NativeKeyBindings final +{ + typedef nsIWidget::NativeKeyBindingsType NativeKeyBindingsType; + typedef nsIWidget::DoCommandCallback DoCommandCallback; + +public: + static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType); + static void Shutdown(); + + void Init(NativeKeyBindingsType aType); + + bool Execute(const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData); + +private: + NativeKeyBindings(); + + SelectorCommandHashtable mSelectorToCommand; + + static NativeKeyBindings* sInstanceForSingleLineEditor; + static NativeKeyBindings* sInstanceForMultiLineEditor; +}; // NativeKeyBindings + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_NativeKeyBindings_h_ diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.mm new file mode 100644 index 000000000..2f4ecadff --- /dev/null +++ b/widget/cocoa/NativeKeyBindings.mm @@ -0,0 +1,292 @@ +/* -*- 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/TextEvents.h" + +namespace mozilla { +namespace widget { + +PRLogModuleInfo* gNativeKeyBindingsLog = nullptr; + +NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr; +NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr; + +// static +NativeKeyBindings* +NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) +{ + switch (aType) { + case nsIWidget::NativeKeyBindingsForSingleLineEditor: + if (!sInstanceForSingleLineEditor) { + sInstanceForSingleLineEditor = new NativeKeyBindings(); + sInstanceForSingleLineEditor->Init(aType); + } + return sInstanceForSingleLineEditor; + case nsIWidget::NativeKeyBindingsForMultiLineEditor: + case nsIWidget::NativeKeyBindingsForRichTextEditor: + 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() +{ +} + +#define SEL_TO_COMMAND(aSel, aCommand) \ + mSelectorToCommand.Put( \ + reinterpret_cast<struct objc_selector *>(@selector(aSel)), aCommand) + +void +NativeKeyBindings::Init(NativeKeyBindingsType aType) +{ + if (!gNativeKeyBindingsLog) { + gNativeKeyBindingsLog = PR_NewLogModule("NativeKeyBindings"); + } + + 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:, CommandCopy); + // SEL_TO_COMMAND(copyFont:, ); + // SEL_TO_COMMAND(copyRuler:, ); + SEL_TO_COMMAND(cut:, CommandCut); + SEL_TO_COMMAND(delete:, CommandDelete); + SEL_TO_COMMAND(deleteBackward:, CommandDeleteCharBackward); + // SEL_TO_COMMAND(deleteBackwardByDecomposingPreviousCharacter:, ); + SEL_TO_COMMAND(deleteForward:, CommandDeleteCharForward); + + // TODO: deleteTo* selectors are also supposed to add text to a kill buffer + SEL_TO_COMMAND(deleteToBeginningOfLine:, CommandDeleteToBeginningOfLine); + SEL_TO_COMMAND(deleteToBeginningOfParagraph:, CommandDeleteToBeginningOfLine); + SEL_TO_COMMAND(deleteToEndOfLine:, CommandDeleteToEndOfLine); + SEL_TO_COMMAND(deleteToEndOfParagraph:, CommandDeleteToEndOfLine); + // SEL_TO_COMMAND(deleteToMark:, ); + + SEL_TO_COMMAND(deleteWordBackward:, CommandDeleteWordBackward); + SEL_TO_COMMAND(deleteWordForward:, CommandDeleteWordForward); + // 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:, CommandCharPrevious); + SEL_TO_COMMAND(moveBackwardAndModifySelection:, CommandSelectCharPrevious); + if (aType == nsIWidget::NativeKeyBindingsForSingleLineEditor) { + SEL_TO_COMMAND(moveDown:, CommandEndLine); + } else { + SEL_TO_COMMAND(moveDown:, CommandLineNext); + } + SEL_TO_COMMAND(moveDownAndModifySelection:, CommandSelectLineNext); + SEL_TO_COMMAND(moveForward:, CommandCharNext); + SEL_TO_COMMAND(moveForwardAndModifySelection:, CommandSelectCharNext); + SEL_TO_COMMAND(moveLeft:, CommandCharPrevious); + SEL_TO_COMMAND(moveLeftAndModifySelection:, CommandSelectCharPrevious); + SEL_TO_COMMAND(moveParagraphBackwardAndModifySelection:, + CommandSelectBeginLine); + SEL_TO_COMMAND(moveParagraphForwardAndModifySelection:, CommandSelectEndLine); + SEL_TO_COMMAND(moveRight:, CommandCharNext); + SEL_TO_COMMAND(moveRightAndModifySelection:, CommandSelectCharNext); + SEL_TO_COMMAND(moveToBeginningOfDocument:, CommandMoveTop); + SEL_TO_COMMAND(moveToBeginningOfDocumentAndModifySelection:, + CommandSelectTop); + SEL_TO_COMMAND(moveToBeginningOfLine:, CommandBeginLine); + SEL_TO_COMMAND(moveToBeginningOfLineAndModifySelection:, + CommandSelectBeginLine); + SEL_TO_COMMAND(moveToBeginningOfParagraph:, CommandBeginLine); + SEL_TO_COMMAND(moveToBeginningOfParagraphAndModifySelection:, + CommandSelectBeginLine); + SEL_TO_COMMAND(moveToEndOfDocument:, CommandMoveBottom); + SEL_TO_COMMAND(moveToEndOfDocumentAndModifySelection:, CommandSelectBottom); + SEL_TO_COMMAND(moveToEndOfLine:, CommandEndLine); + SEL_TO_COMMAND(moveToEndOfLineAndModifySelection:, CommandSelectEndLine); + SEL_TO_COMMAND(moveToEndOfParagraph:, CommandEndLine); + SEL_TO_COMMAND(moveToEndOfParagraphAndModifySelection:, CommandSelectEndLine); + SEL_TO_COMMAND(moveToLeftEndOfLine:, CommandBeginLine); + SEL_TO_COMMAND(moveToLeftEndOfLineAndModifySelection:, + CommandSelectBeginLine); + SEL_TO_COMMAND(moveToRightEndOfLine:, CommandEndLine); + SEL_TO_COMMAND(moveToRightEndOfLineAndModifySelection:, CommandSelectEndLine); + if (aType == nsIWidget::NativeKeyBindingsForSingleLineEditor) { + SEL_TO_COMMAND(moveUp:, CommandBeginLine); + } else { + SEL_TO_COMMAND(moveUp:, CommandLinePrevious); + } + SEL_TO_COMMAND(moveUpAndModifySelection:, CommandSelectLinePrevious); + SEL_TO_COMMAND(moveWordBackward:, CommandWordPrevious); + SEL_TO_COMMAND(moveWordBackwardAndModifySelection:, + CommandSelectWordPrevious); + SEL_TO_COMMAND(moveWordForward:, CommandWordNext); + SEL_TO_COMMAND(moveWordForwardAndModifySelection:, CommandSelectWordNext); + SEL_TO_COMMAND(moveWordLeft:, CommandWordPrevious); + SEL_TO_COMMAND(moveWordLeftAndModifySelection:, CommandSelectWordPrevious); + SEL_TO_COMMAND(moveWordRight:, CommandWordNext); + SEL_TO_COMMAND(moveWordRightAndModifySelection:, CommandSelectWordNext); + SEL_TO_COMMAND(pageDown:, CommandMovePageDown); + SEL_TO_COMMAND(pageDownAndModifySelection:, CommandSelectPageDown); + SEL_TO_COMMAND(pageUp:, CommandMovePageUp); + SEL_TO_COMMAND(pageUpAndModifySelection:, CommandSelectPageUp); + SEL_TO_COMMAND(paste:, CommandPaste); + // SEL_TO_COMMAND(pasteFont:, ); + // SEL_TO_COMMAND(pasteRuler:, ); + SEL_TO_COMMAND(scrollLineDown:, CommandScrollLineDown); + SEL_TO_COMMAND(scrollLineUp:, CommandScrollLineUp); + SEL_TO_COMMAND(scrollPageDown:, CommandScrollPageDown); + SEL_TO_COMMAND(scrollPageUp:, CommandScrollPageUp); + SEL_TO_COMMAND(scrollToBeginningOfDocument:, CommandScrollTop); + SEL_TO_COMMAND(scrollToEndOfDocument:, CommandScrollBottom); + SEL_TO_COMMAND(selectAll:, CommandSelectAll); + // selectLine: is complex, see KeyDown + if (aType == nsIWidget::NativeKeyBindingsForSingleLineEditor) { + SEL_TO_COMMAND(selectParagraph:, CommandSelectAll); + } + // 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 + +bool +NativeKeyBindings::Execute(const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) +{ + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress", 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] != NSKeyDown) { + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, no Cocoa key down event", this)); + + return false; + } + + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, interpreting", this)); + + AutoTArray<KeyBindingsCommand, 2> bindingCommands; + nsCocoaUtils::GetCommandsFromKeyEvent(cocoaEvent, bindingCommands); + + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, bindingCommands=%u", + this, bindingCommands.Length())); + + AutoTArray<Command, 4> geckoCommands; + + 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::KeyPress, selector=%s", + this, NS_LossyConvertUTF16toASCII(nsSelectorString).get())); + } + + // Try to find a simple mapping in the hashtable + Command geckoCommand = static_cast<Command>(mSelectorToCommand.Get( + reinterpret_cast<struct objc_selector*>(selector))); + + if (geckoCommand) { + geckoCommands.AppendElement(geckoCommand); + } else if (selector == @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. + geckoCommands.AppendElement(CommandBeginLine); + geckoCommands.AppendElement(CommandSelectEndLine); + } else if (selector == @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. + geckoCommands.AppendElement(CommandWordPrevious); + geckoCommands.AppendElement(CommandSelectWordNext); + } + } + + if (geckoCommands.IsEmpty()) { + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, handled=false", this)); + + return false; + } + + for (uint32_t i = 0; i < geckoCommands.Length(); i++) { + Command geckoCommand = geckoCommands[i]; + + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, command=%s", + this, WidgetKeyboardEvent::GetCommandStr(geckoCommand))); + + // Execute the Gecko command + aCallback(geckoCommand, aCallbackData); + } + + MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, + ("%p NativeKeyBindings::KeyPress, handled=true", this)); + + return true; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/cocoa/OSXNotificationCenter.h b/widget/cocoa/OSXNotificationCenter.h new file mode 100644 index 000000000..30767b5c5 --- /dev/null +++ b/widget/cocoa/OSXNotificationCenter.h @@ -0,0 +1,55 @@ +/* -*- 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 "imgINotificationObserver.h" +#include "nsITimer.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" + +@class mozNotificationCenterDelegate; + +#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) +typedef NSInteger NSUserNotificationActivationType; +#endif + +namespace mozilla { + +class OSXNotificationInfo; + +class OSXNotificationCenter : public nsIAlertsService, + public nsIAlertsIconData, + public nsIAlertNotificationImageListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIALERTSSERVICE + NS_DECL_NSIALERTSICONDATA + 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; +}; + +} // namespace mozilla + +#endif // OSXNotificationCenter_h diff --git a/widget/cocoa/OSXNotificationCenter.mm b/widget/cocoa/OSXNotificationCenter.mm new file mode 100644 index 000000000..e9e36a96b --- /dev/null +++ b/widget/cocoa/OSXNotificationCenter.mm @@ -0,0 +1,589 @@ +/* -*- 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 + +#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) +@protocol NSUserNotificationCenterDelegate +@end +static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName"; +enum { + NSUserNotificationActivationTypeNone = 0, + NSUserNotificationActivationTypeContentsClicked = 1, + NSUserNotificationActivationTypeActionButtonClicked = 2, +}; +#endif + +#if !defined(MAC_OS_X_VERSION_10_9) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9) +enum { + NSUserNotificationActivationTypeReplied = 3, +}; +#endif + +#if !defined(MAC_OS_X_VERSION_10_10) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10) +enum { + NSUserNotificationActivationTypeAdditionalActionClicked = 4 +}; +#endif + +@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> mPendingNotifiction; +}; + +NS_IMPL_ISUPPORTS0(OSXNotificationInfo) + +OSXNotificationInfo::OSXNotificationInfo(NSString *name, nsIObserver *observer, + const nsAString & alertCookie) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!"); + mName = [name retain]; + mObserver = observer; + mCookie = alertCookie; + mPendingNotifiction = nil; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +OSXNotificationInfo::~OSXNotificationInfo() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mName release]; + [mPendingNotifiction release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +static id<FakeNSUserNotificationCenter> GetNotificationCenter() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + Class c = NSClassFromString(@"NSUserNotificationCenter"); + return [c performSelector:@selector(defaultUserNotificationCenter)]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +OSXNotificationCenter::OSXNotificationCenter() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this]; + GetNotificationCenter().delegate = mDelegate; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +OSXNotificationCenter::~OSXNotificationCenter() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [GetNotificationCenter() removeAllDeliveredNotifications]; + [mDelegate release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData, + nsIAlertNotificationImageListener) + +nsresult OSXNotificationCenter::Init() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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); + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, + aAlertText, aAlertTextClickable, + aAlertCookie, aBidi, aLang, aData, + aPrincipal, aInPrivateBrowsing, + aRequireInteraction); + 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_ABORT_BLOCK_NSRESULT; + + NS_ENSURE_ARG(aAlert); + + 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) { + const char16_t* formatStrings[] = { hostPort.get() }; + nsXPIDLString notificationSource; + bundle->FormatStringFromName(u"source.label", + formatStrings, + ArrayLength(formatStrings), + getter_Copies(notificationSource)); + notification.subtitle = nsCocoaUtils::ToNSString(notificationSource); + } + + nsAutoString text; + rv = aAlert->GetText(text); + NS_ENSURE_SUCCESS(rv, rv); + notification.informativeText = nsCocoaUtils::ToNSString(text); + + notification.soundName = 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) { + nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle; + bundle->GetStringFromName(u"closeButton.title", + getter_Copies(closeButtonTitle)); + bundle->GetStringFromName(u"actionButton.label", + getter_Copies(actionButtonTitle)); + if (!hostPort.IsEmpty()) { + const char16_t* formatStrings[] = { hostPort.get() }; + bundle->FormatStringFromName(u"webActions.disableForOrigin.label", + formatStrings, + ArrayLength(formatStrings), + getter_Copies(disableButtonTitle)); + } + bundle->GetStringFromName(u"webActions.settings.label", + getter_Copies(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->mPendingNotifiction = 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_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, + nsIPrincipal* aPrincipal) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSString *alertName = nsCocoaUtils::ToNSString(aAlertName); + CloseAlertCocoaString(alertName); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void +OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +void +OSXNotificationCenter::OnActivate(NSString *aAlertName, + NSUserNotificationActivationType aActivationType, + unsigned long long aAdditionalActionIndex) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +void +OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo *osxni) +{ + NS_OBJC_BEGIN_TRY_ABORT_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->mPendingNotifiction]; + + if (osxni->mObserver) { + osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get()); + } + + [osxni->mPendingNotifiction release]; + osxni->mPendingNotifiction = nil; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP +OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + OSXNotificationInfo *osxni = static_cast<OSXNotificationInfo*>(aUserData); + if (osxni->mPendingNotifiction) { + // 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_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +OSXNotificationCenter::OnImageReady(nsISupports* aUserData, + imgIRequest* aRequest) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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->mPendingNotifiction) { + return NS_ERROR_FAILURE; + } + + NSImage *cocoaImage = nil; + nsCocoaUtils::CreateNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST, &cocoaImage, 1.0f); + (osxni->mPendingNotifiction).contentImage = cocoaImage; + [cocoaImage release]; + ShowPendingNotification(osxni); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +} // namespace mozilla diff --git a/widget/cocoa/RectTextureImage.h b/widget/cocoa/RectTextureImage.h new file mode 100644 index 000000000..022b216c6 --- /dev/null +++ b/widget/cocoa/RectTextureImage.h @@ -0,0 +1,80 @@ +/* -*- 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 RectTextureImage_h_ +#define RectTextureImage_h_ + +#include "mozilla/RefPtr.h" + +class MacIOSurface; + +namespace mozilla { + +namespace gl { +class GLContext; +} // namespace gl + +namespace widget { + +// Manages a texture which can resize dynamically, binds to the +// LOCAL_GL_TEXTURE_RECTANGLE_ARB texture target and is automatically backed +// by a power-of-two size GL texture. The latter two features are used for +// compatibility with older Mac hardware which we block GL layers on. +// RectTextureImages are used both for accelerated GL layers drawing and for +// OMTC BasicLayers drawing. +class RectTextureImage { +public: + RectTextureImage(); + + virtual ~RectTextureImage(); + + already_AddRefed<gfx::DrawTarget> + BeginUpdate(const LayoutDeviceIntSize& aNewSize, + const LayoutDeviceIntRegion& aDirtyRegion = + LayoutDeviceIntRegion()); + void EndUpdate(); + + void UpdateIfNeeded(const LayoutDeviceIntSize& aNewSize, + const LayoutDeviceIntRegion& aDirtyRegion, + void (^aCallback)(gfx::DrawTarget*, + const LayoutDeviceIntRegion&)) + { + RefPtr<gfx::DrawTarget> drawTarget = BeginUpdate(aNewSize, aDirtyRegion); + if (drawTarget) { + aCallback(drawTarget, GetUpdateRegion()); + EndUpdate(); + } + } + + void UpdateFromCGContext(const LayoutDeviceIntSize& aNewSize, + const LayoutDeviceIntRegion& aDirtyRegion, + CGContextRef aCGContext); + + LayoutDeviceIntRegion GetUpdateRegion() { + MOZ_ASSERT(mInUpdate, "update region only valid during update"); + return mUpdateRegion; + } + + void Draw(mozilla::layers::GLManager* aManager, + const LayoutDeviceIntPoint& aLocation, + const gfx::Matrix4x4& aTransform = gfx::Matrix4x4()); + + +protected: + void DeleteTexture(); + void BindIOSurfaceToTexture(gl::GLContext* aGL); + + RefPtr<MacIOSurface> mIOSurface; + gl::GLContext* mGLContext; + LayoutDeviceIntRegion mUpdateRegion; + LayoutDeviceIntSize mBufferSize; + GLuint mTexture; + bool mInUpdate; +}; + +} // namespace widget +} // namespace mozilla + +#endif // RectTextureImage_h_ diff --git a/widget/cocoa/RectTextureImage.mm b/widget/cocoa/RectTextureImage.mm new file mode 100644 index 000000000..c67af97d0 --- /dev/null +++ b/widget/cocoa/RectTextureImage.mm @@ -0,0 +1,171 @@ +/* -*- 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 "RectTextureImage.h" + +#include "gfxUtils.h" +#include "GLContextCGL.h" +#include "mozilla/layers/GLManager.h" +#include "mozilla/gfx/MacIOSurface.h" +#include "OGLShaderProgram.h" +#include "ScopedGLHelpers.h" + +namespace mozilla { +namespace widget { + +RectTextureImage::RectTextureImage() + : mGLContext(nullptr) + , mTexture(0) + , mInUpdate(false) +{ +} + +RectTextureImage::~RectTextureImage() +{ + DeleteTexture(); +} + +already_AddRefed<gfx::DrawTarget> +RectTextureImage::BeginUpdate(const LayoutDeviceIntSize& aNewSize, + const LayoutDeviceIntRegion& aDirtyRegion) +{ + MOZ_ASSERT(!mInUpdate, "Beginning update during update!"); + mUpdateRegion = aDirtyRegion; + bool needRecreate = false; + if (aNewSize != mBufferSize) { + mBufferSize = aNewSize; + mUpdateRegion = + LayoutDeviceIntRect(LayoutDeviceIntPoint(0, 0), aNewSize); + needRecreate = true; + } + + if (mUpdateRegion.IsEmpty()) { + return nullptr; + } + + if (!mIOSurface || needRecreate) { + DeleteTexture(); + mIOSurface = MacIOSurface::CreateIOSurface(mBufferSize.width, + mBufferSize.height); + + if (!mIOSurface) { + return nullptr; + } + } + + mInUpdate = true; + + mIOSurface->Lock(false); + unsigned char* ioData = (unsigned char*)mIOSurface->GetBaseAddress(); + gfx::IntSize size(mBufferSize.width, mBufferSize.height); + int32_t stride = mIOSurface->GetBytesPerRow(); + gfx::SurfaceFormat format = gfx::SurfaceFormat::B8G8R8A8; + RefPtr<gfx::DrawTarget> drawTarget = + gfx::Factory::CreateDrawTargetForData(gfx::BackendType::SKIA, + ioData, size, + stride, format); + return drawTarget.forget(); +} + +void +RectTextureImage::EndUpdate() +{ + MOZ_ASSERT(mInUpdate, "Ending update while not in update"); + mIOSurface->Unlock(false); + mInUpdate = false; +} + +void +RectTextureImage::UpdateFromCGContext(const LayoutDeviceIntSize& aNewSize, + const LayoutDeviceIntRegion& aDirtyRegion, + CGContextRef aCGContext) +{ + gfx::IntSize size = gfx::IntSize(CGBitmapContextGetWidth(aCGContext), + CGBitmapContextGetHeight(aCGContext)); + RefPtr<gfx::DrawTarget> dt = BeginUpdate(aNewSize, aDirtyRegion); + if (dt) { + gfx::Rect rect(0, 0, size.width, size.height); + gfxUtils::ClipToRegion(dt, GetUpdateRegion().ToUnknownRegion()); + RefPtr<gfx::SourceSurface> sourceSurface = + dt->CreateSourceSurfaceFromData(static_cast<uint8_t *>(CGBitmapContextGetData(aCGContext)), + size, + CGBitmapContextGetBytesPerRow(aCGContext), + gfx::SurfaceFormat::B8G8R8A8); + dt->DrawSurface(sourceSurface, rect, rect, + gfx::DrawSurfaceOptions(), + gfx::DrawOptions(1.0, gfx::CompositionOp::OP_SOURCE)); + dt->PopClip(); + EndUpdate(); + } +} + +void +RectTextureImage::Draw(layers::GLManager* aManager, + const LayoutDeviceIntPoint& aLocation, + const gfx::Matrix4x4& aTransform) +{ + gl::GLContext* gl = aManager->gl(); + + BindIOSurfaceToTexture(gl); + + layers::ShaderProgramOGL* program = + aManager->GetProgram(LOCAL_GL_TEXTURE_RECTANGLE_ARB, + gfx::SurfaceFormat::R8G8B8A8); + + gl->fActiveTexture(LOCAL_GL_TEXTURE0); + gl::ScopedBindTexture texture(gl, mTexture, LOCAL_GL_TEXTURE_RECTANGLE_ARB); + + aManager->ActivateProgram(program); + program->SetProjectionMatrix(aManager->GetProjMatrix()); + program->SetLayerTransform(gfx::Matrix4x4(aTransform).PostTranslate(aLocation.x, aLocation.y, 0)); + program->SetTextureTransform(gfx::Matrix4x4()); + program->SetRenderOffset(nsIntPoint(0, 0)); + program->SetTexCoordMultiplier(mBufferSize.width, mBufferSize.height); + program->SetTextureUnit(0); + + aManager->BindAndDrawQuad(program, + gfx::Rect(0.0, 0.0, mBufferSize.width, mBufferSize.height), + gfx::Rect(0.0, 0.0, 1.0f, 1.0f)); +} + +void +RectTextureImage::DeleteTexture() +{ + if (mTexture) { + MOZ_ASSERT(mGLContext); + mGLContext->MakeCurrent(); + mGLContext->fDeleteTextures(1, &mTexture); + mTexture = 0; + } +} + +void +RectTextureImage::BindIOSurfaceToTexture(gl::GLContext* aGL) +{ + if (!mTexture) { + MOZ_ASSERT(aGL); + aGL->fGenTextures(1, &mTexture); + aGL->fActiveTexture(LOCAL_GL_TEXTURE0); + gl::ScopedBindTexture texture(aGL, mTexture, LOCAL_GL_TEXTURE_RECTANGLE_ARB); + aGL->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB, + LOCAL_GL_TEXTURE_MIN_FILTER, + LOCAL_GL_LINEAR); + aGL->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB, + LOCAL_GL_TEXTURE_MAG_FILTER, + LOCAL_GL_LINEAR); + aGL->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB, + LOCAL_GL_TEXTURE_WRAP_T, + LOCAL_GL_CLAMP_TO_EDGE); + aGL->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB, + LOCAL_GL_TEXTURE_WRAP_S, + LOCAL_GL_CLAMP_TO_EDGE); + + mIOSurface->CGLTexImageIOSurface2D(gl::GLContextCGL::Cast(aGL)->GetCGLContext()); + mGLContext = aGL; + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/cocoa/SwipeTracker.h b/widget/cocoa/SwipeTracker.h new file mode 100644 index 000000000..b5eb2a481 --- /dev/null +++ b/widget/cocoa/SwipeTracker.h @@ -0,0 +1,96 @@ +/* -*- 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 SwipeTracker_h +#define SwipeTracker_h + +#include "EventForwards.h" +#include "mozilla/layers/AxisPhysicsMSDModel.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsRefreshDriver.h" +#include "Units.h" + +class nsIPresShell; + +namespace mozilla { + +class PanGestureInput; + +/** + * SwipeTracker turns PanGestureInput events into swipe events + * (WidgetSimpleGestureEvent) and dispatches them into Gecko. + * The swiping behavior mirrors the behavior of the Cocoa API + * -[NSEvent trackSwipeEventWithOptions:dampenAmountThresholdMin:max:usingHandler:]. + * The advantage of using this class over the Cocoa API is that this class + * properly supports submitting queued up events to it, and that it hopefully + * doesn't intermittently break scrolling the way the Cocoa API does (bug 927702). + * + * The swipe direction is either left or right. It is determined before the + * SwipeTracker is created and stays fixed during the swipe. + * During the swipe, the swipe has a current "value" which is between 0 and the + * target value. The target value is either 1 (swiping left) or -1 (swiping + * right) - see SwipeSuccessTargetValue(). + * A swipe can either succeed or fail. If it succeeds, the swipe animation + * animates towards the success target value; if it fails, it animates back to + * a value of 0. A swipe can only succeed if the user is swiping in an allowed + * direction. (Since both the allowed directions and the swipe direction are + * known at swipe start time, it's clear from the beginning whether a swipe is + * doomed to fail. In that case, the purpose of the SwipeTracker is to simulate + * a bounce-back animation.) + */ +class SwipeTracker final : public nsARefreshObserver { +public: + NS_INLINE_DECL_REFCOUNTING(SwipeTracker, override) + + SwipeTracker(nsChildView& aWidget, + const PanGestureInput& aSwipeStartEvent, + uint32_t aAllowedDirections, + uint32_t aSwipeDirection); + + void Destroy(); + + nsEventStatus ProcessEvent(const PanGestureInput& aEvent); + void CancelSwipe(); + + static WidgetSimpleGestureEvent + CreateSwipeGestureEvent(EventMessage aMsg, nsIWidget* aWidget, + const LayoutDeviceIntPoint& aPosition); + + + // nsARefreshObserver + void WillRefresh(mozilla::TimeStamp aTime) override; + +protected: + ~SwipeTracker(); + + bool SwipingInAllowedDirection() const { return mAllowedDirections & mSwipeDirection; } + double SwipeSuccessTargetValue() const; + double ClampToAllowedRange(double aGestureAmount) const; + bool ComputeSwipeSuccess() const; + void StartAnimating(double aTargetValue); + void SwipeFinished(); + void UnregisterFromRefreshDriver(); + bool SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta); + + nsChildView& mWidget; + RefPtr<nsRefreshDriver> mRefreshDriver; + layers::AxisPhysicsMSDModel mAxis; + const LayoutDeviceIntPoint mEventPosition; + TimeStamp mLastEventTimeStamp; + TimeStamp mLastAnimationFrameTime; + const uint32_t mAllowedDirections; + const uint32_t mSwipeDirection; + double mGestureAmount; + double mCurrentVelocity; + bool mEventsAreControllingSwipe; + bool mEventsHaveStartedNewGesture; + bool mRegisteredWithRefreshDriver; +}; + +} // namespace mozilla + +#endif // SwipeTracker_h diff --git a/widget/cocoa/SwipeTracker.mm b/widget/cocoa/SwipeTracker.mm new file mode 100644 index 000000000..9c06ee0c3 --- /dev/null +++ b/widget/cocoa/SwipeTracker.mm @@ -0,0 +1,220 @@ +/* -*- 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 "SwipeTracker.h" + +#include "InputData.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/TouchEvents.h" +#include "nsAlgorithm.h" +#include "nsChildView.h" +#include "UnitTransforms.h" + +// These values were tweaked to make the physics feel similar to the native swipe. +static const double kSpringForce = 250.0; +static const double kVelocityTwitchTolerance = 0.0000001; +static const double kWholePagePixelSize = 1000.0; +static const double kRubberBandResistanceFactor = 4.0; +static const double kSwipeSuccessThreshold = 0.25; +static const double kSwipeSuccessVelocityContribution = 0.3; + +namespace mozilla { + +static already_AddRefed<nsRefreshDriver> +GetRefreshDriver(nsIWidget& aWidget) +{ + nsIWidgetListener* widgetListener = aWidget.GetWidgetListener(); + nsIPresShell* presShell = widgetListener ? widgetListener->GetPresShell() : nullptr; + nsPresContext* presContext = presShell ? presShell->GetPresContext() : nullptr; + RefPtr<nsRefreshDriver> refreshDriver = presContext ? presContext->RefreshDriver() : nullptr; + return refreshDriver.forget(); +} + +SwipeTracker::SwipeTracker(nsChildView& aWidget, + const PanGestureInput& aSwipeStartEvent, + uint32_t aAllowedDirections, + uint32_t aSwipeDirection) + : mWidget(aWidget) + , mRefreshDriver(GetRefreshDriver(mWidget)) + , mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0) + , mEventPosition(RoundedToInt(ViewAs<LayoutDevicePixel>(aSwipeStartEvent.mPanStartPoint, + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))) + , mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp) + , mAllowedDirections(aAllowedDirections) + , mSwipeDirection(aSwipeDirection) + , mGestureAmount(0.0) + , mCurrentVelocity(0.0) + , mEventsAreControllingSwipe(true) + , mEventsHaveStartedNewGesture(false) + , mRegisteredWithRefreshDriver(false) +{ + SendSwipeEvent(eSwipeGestureStart, 0, 0.0); + ProcessEvent(aSwipeStartEvent); +} + +void +SwipeTracker::Destroy() +{ + UnregisterFromRefreshDriver(); +} + +SwipeTracker::~SwipeTracker() +{ + MOZ_ASSERT(!mRegisteredWithRefreshDriver, "Destroy needs to be called before deallocating"); +} + +double +SwipeTracker::SwipeSuccessTargetValue() const +{ + return (mSwipeDirection == nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1.0 : 1.0; +} + +double +SwipeTracker::ClampToAllowedRange(double aGestureAmount) const +{ + // gestureAmount needs to stay between -1 and 0 when swiping right and + // between 0 and 1 when swiping left. + double min = (mSwipeDirection == nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1.0 : 0.0; + double max = (mSwipeDirection == nsIDOMSimpleGestureEvent::DIRECTION_LEFT) ? 1.0 : 0.0; + return clamped(aGestureAmount, min, max); +} + +bool +SwipeTracker::ComputeSwipeSuccess() const +{ + double targetValue = SwipeSuccessTargetValue(); + + // If the fingers were moving away from the target direction when they were + // lifted from the touchpad, abort the swipe. + if (mCurrentVelocity * targetValue < -kVelocityTwitchTolerance) { + return false; + } + + return (mGestureAmount * targetValue + + mCurrentVelocity * targetValue * kSwipeSuccessVelocityContribution) >= kSwipeSuccessThreshold; +} + +nsEventStatus +SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) +{ + // If the fingers have already been lifted, don't process this event for swiping. + if (!mEventsAreControllingSwipe) { + // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture + // and nsEventStatus_eIgnore for events of subsequent scroll gestures. + if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART || + aEvent.mType == PanGestureInput::PANGESTURE_START) { + mEventsHaveStartedNewGesture = true; + } + return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore : nsEventStatus_eConsumeNoDefault; + } + + double delta = -aEvent.mPanDisplacement.x / mWidget.BackingScaleFactor() / kWholePagePixelSize; + if (!SwipingInAllowedDirection()) { + delta /= kRubberBandResistanceFactor; + } + mGestureAmount = ClampToAllowedRange(mGestureAmount + delta); + SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount); + + if (aEvent.mType != PanGestureInput::PANGESTURE_END) { + double elapsedSeconds = std::max(0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds()); + mCurrentVelocity = delta / elapsedSeconds; + mLastEventTimeStamp = aEvent.mTimeStamp; + } else { + mEventsAreControllingSwipe = false; + bool didSwipeSucceed = SwipingInAllowedDirection() && ComputeSwipeSuccess(); + double targetValue = 0.0; + if (didSwipeSucceed) { + SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0); + targetValue = SwipeSuccessTargetValue(); + } + StartAnimating(targetValue); + } + + return nsEventStatus_eConsumeNoDefault; +} + +void +SwipeTracker::StartAnimating(double aTargetValue) +{ + mAxis.SetPosition(mGestureAmount); + mAxis.SetDestination(aTargetValue); + mAxis.SetVelocity(mCurrentVelocity); + + mLastAnimationFrameTime = TimeStamp::Now(); + + // Add ourselves as a refresh driver observer. The refresh driver + // will call WillRefresh for each animation frame until we + // unregister ourselves. + MOZ_ASSERT(!mRegisteredWithRefreshDriver); + if (mRefreshDriver) { + mRefreshDriver->AddRefreshObserver(this, Flush_Style); + mRegisteredWithRefreshDriver = true; + } +} + +void +SwipeTracker::WillRefresh(mozilla::TimeStamp aTime) +{ + TimeStamp now = TimeStamp::Now(); + mAxis.Simulate(now - mLastAnimationFrameTime); + mLastAnimationFrameTime = now; + + bool isFinished = mAxis.IsFinished(1.0 / kWholePagePixelSize); + mGestureAmount = (isFinished ? mAxis.GetDestination() : mAxis.GetPosition()); + SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount); + + if (isFinished) { + UnregisterFromRefreshDriver(); + SwipeFinished(); + } +} + +void +SwipeTracker::CancelSwipe() +{ + SendSwipeEvent(eSwipeGestureEnd, 0, 0.0); +} + +void SwipeTracker::SwipeFinished() +{ + SendSwipeEvent(eSwipeGestureEnd, 0, 0.0); + mWidget.SwipeFinished(); +} + +void +SwipeTracker::UnregisterFromRefreshDriver() +{ + if (mRegisteredWithRefreshDriver) { + MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?"); + mRefreshDriver->RemoveRefreshObserver(this, Flush_Style); + } + mRegisteredWithRefreshDriver = false; +} + +/* static */ WidgetSimpleGestureEvent +SwipeTracker::CreateSwipeGestureEvent(EventMessage aMsg, nsIWidget* aWidget, + const LayoutDeviceIntPoint& aPosition) +{ + WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget); + geckoEvent.mModifiers = 0; + geckoEvent.mTimeStamp = TimeStamp::Now(); + geckoEvent.mRefPoint = aPosition; + geckoEvent.buttons = 0; + return geckoEvent; +} + +bool +SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta) +{ + WidgetSimpleGestureEvent geckoEvent = + CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition); + geckoEvent.mDirection = aDirection; + geckoEvent.mDelta = aDelta; + geckoEvent.mAllowedDirections = mAllowedDirections; + return mWidget.DispatchWindowEvent(geckoEvent); +} + +} // namespace mozilla diff --git a/widget/cocoa/TextInputHandler.h b/widget/cocoa/TextInputHandler.h new file mode 100644 index 000000000..de7c77593 --- /dev/null +++ b/widget/cocoa/TextInputHandler.h @@ -0,0 +1,1195 @@ +/* -*- 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/EventForwards.h" +#include "mozilla/TextEventDispatcherListener.h" +#include "WritingModes.h" + +class nsChildView; + +namespace mozilla { +namespace widget { + +// Key code constants +enum +{ +#if !defined(MAC_OS_X_VERSION_10_12) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 + kVK_RightCommand = 0x36, // right command key +#endif + + 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() + { + mInputSourceList = nullptr; + Clear(); + } + + explicit TISInputSourceWrapper(const char* aID) + { + mInputSourceList = nullptr; + InitByInputSourceID(aID); + } + + explicit TISInputSourceWrapper(SInt32 aLayoutID) + { + mInputSourceList = nullptr; + InitByLayoutID(aLayoutID); + } + + explicit TISInputSourceWrapper(TISInputSourceRef aInputSource) + { + mInputSourceList = nullptr; + InitByTISInputSourceRef(aInputSource); + } + + ~TISInputSourceWrapper() { Clear(); } + + void InitByInputSourceID(const char* aID); + void InitByInputSourceID(const nsAFlatString &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 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 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, + 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 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, + 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. + */ + static CodeNameIndex ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode); + +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); + + /** + * 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 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, + 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; + // 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) + { + Clear(); + } + + explicit KeyEventState(NSEvent* aNativeKeyEvent) : mKeyEvent(nullptr) + { + Clear(); + Set(aNativeKeyEvent); + } + + KeyEventState(const KeyEventState &aOther) = delete; + + ~KeyEventState() + { + Clear(); + } + + void Set(NSEvent* aNativeKeyEvent) + { + NS_PRECONDITION(aNativeKeyEvent, "aNativeKeyEvent must not be NULL"); + Clear(); + mKeyEvent = [aNativeKeyEvent retain]; + } + + void Clear() + { + if (mKeyEvent) { + [mKeyEvent release]; + mKeyEvent = nullptr; + } + mInsertString = nullptr; + mInsertedString.Truncate(); + mKeyDownHandled = false; + mKeyPressDispatched = false; + mKeyPressHandled = false; + mCausedOtherKeyEvents = false; + mCompositionDispatched = false; + } + + bool IsDefaultPrevented() const + { + return mKeyDownHandled || mKeyPressHandled || mCausedOtherKeyEvents || + mCompositionDispatched; + } + + bool CanDispatchKeyPressEvent() const + { + return !mKeyPressDispatched && !IsDefaultPrevented(); + } + + void InitKeyEvent(TextInputHandlerBase* aHandler, + WidgetKeyboardEvent& aKeyEvent); + + /** + * 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 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); + keyEvent = &mFirstKeyEvent; + } else { + keyEvent = new KeyEventState(aNativeKeyEvent); + } + 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 = GetCurrentKeyEvent(); + mCurrentKeyEvents.RemoveElementAt(mCurrentKeyEvents.Length() - 1); + 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 aKeyEvent causes text input. + * + * @param aKeyEvent A key event. + * @return TRUE if the key event causes text input. + * Otherwise, FALSE. + */ + static bool IsNormalCharInputtingEvent(const WidgetKeyboardEvent& aKeyEvent); + + /** + * 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_(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); + + /** + * 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 IsIMEOpened(); + bool IsIMEEnabled() { return mIsIMEEnabled; } + bool IsASCIICapableOnly() { return mIsASCIICapableOnly; } + bool IgnoreIMECommit() { return mIgnoreIMECommit; } + + bool IgnoreIMEComposition() + { + // Ignore the IME composition events when we're pending to discard the + // composition and we are not to handle the IME composition now. + return (mPendingMethods & kDiscardIMEComposition) && + (mIsInFocusProcessing || !IsFocused()); + } + + 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, + kDiscardIMEComposition = 2, + kSyncASCIICapableOnly = 4 + }; + 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); + +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; + bool mIsIMEEnabled; + bool mIsASCIICapableOnly; + bool mIgnoreIMECommit; + // This flag is enabled by OnFocusChangeInGecko, and will be cleared by + // ExecutePendingMethods. When this is true, IsFocus() returns TRUE. At + // that time, the focus processing in Gecko might not be finished yet. So, + // you cannot use WidgetQueryContentEvent or something. + bool mIsInFocusProcessing; + bool mIMEHasFocus; + + void KillIMEComposition(); + void SendCommittedText(NSString *aString); + void OpenSystemPreferredLanguageIME(); + + // Pending methods + void NotifyIMEOfFocusChangeInGecko(); + void DiscardIMEComposition(); + 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 NSKeyDown event. + * @return TRUE if the event is consumed by web contents + * or chrome contents. Otherwise, FALSE. + */ + bool HandleKeyDownEvent(NSEvent* aNativeEvent); + + /** + * KeyUp event handler. + * + * @param aNativeEvent A native NSKeyUp event. + */ + void HandleKeyUpEvent(NSEvent* aNativeEvent); + + /** + * FlagsChanged event handler. + * + * @param aNativeEvent A native NSFlagsChanged 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); + + /** + * 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 & ~NSDeviceIndependentModifierFlagsMask); + } + + NSUInteger GetDeviceIndependentFlags() const + { + return (flags & NSDeviceIndependentModifierFlagsMask); + } + }; + 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 000000000..39965d013 --- /dev/null +++ b/widget/cocoa/TextInputHandler.mm @@ -0,0 +1,4534 @@ +/* -*- 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/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" + +#include "nsChildView.h" +#include "nsObjCExceptions.h" +#include "nsBidiUtils.h" +#include "nsToolkit.h" +#include "nsCocoaUtils.h" +#include "WidgetUtils.h" +#include "nsPrintfCString.h" +#include "ComplexTextInputPanel.h" + +using namespace mozilla; +using namespace mozilla::widget; + +LazyLogModule gLog("TextInputHandlerWidgets"); + +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 NSString* aString) +{ + nsAutoString str; + nsCocoaUtils::GetStringForNSString(aString, str); + if (str.IsEmpty()) { + return ""; + } + + nsAutoString escapedStr; + for (uint32_t i = 0; i < str.Length(); i++) { + char16_t ch = str[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 CFStringRef aString) +{ + const NSString* str = reinterpret_cast<const NSString*>(aString); + return GetCharacters(str); +} + +static const char* +GetNativeKeyEventType(NSEvent* aNativeEvent) +{ + switch ([aNativeEvent type]) { + case NSKeyDown: return "NSKeyDown"; + case NSKeyUp: return "NSKeyUp"; + case NSFlagsChanged: return "NSFlagsChanged"; + 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(); + } +} + +#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(gLog, 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, aKeyCode, aModifiers, 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(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%llu", + this, err, len)); + + NS_ENSURE_TRUE(err == noErr, false); + if (len == 0) { + return true; + } + NS_ENSURE_TRUE(EnsureStringLength(aStr, len), 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(gLog, 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)); +} + +void +TISInputSourceWrapper::InitByInputSourceID(const char* aID) +{ + Clear(); + if (!aID) + return; + + CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID, + kCFStringEncodingASCII); + InitByInputSourceID(idstr); + ::CFRelease(idstr); +} + +void +TISInputSourceWrapper::InitByInputSourceID(const nsAFlatString &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 = UCS2_CHAR_IS_BIDI(ch); + } + return mIsRTL != 0; +} + +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] != NSKeyDown && + [aNativeKeyEvent type] != NSKeyUp) { + NS_WARNING("Why the printable key doesn't cause NSKeyDown or NSKeyUp?"); + 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, + const nsAString *aInsertString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::InitKeyEvent, aNativeKeyEvent=%p, " + "aKeyEvent.mMessage=%s, aInsertString=%p, IsOpenedIMEMode()=%s", + this, aNativeKeyEvent, GetGeckoKeyEventType(aKeyEvent), aInsertString, + TrueOrFalse(IsOpenedIMEMode()))); + + NS_ENSURE_TRUE(aNativeKeyEvent, ); + + 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; + + // Fill in fields used for Cocoa NPAPI plugins + if ([aNativeKeyEvent type] == NSKeyDown || + [aNativeKeyEvent type] == NSKeyUp) { + aKeyEvent.mNativeKeyCode = [aNativeKeyEvent keyCode]; + aKeyEvent.mNativeModifierFlags = [aNativeKeyEvent modifierFlags]; + nsAutoString nativeChars; + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], nativeChars); + aKeyEvent.mNativeCharacters.Assign(nativeChars); + nsAutoString nativeCharsIgnoringModifiers; + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent charactersIgnoringModifiers], nativeCharsIgnoringModifiers); + aKeyEvent.mNativeCharactersIgnoringModifiers.Assign(nativeCharsIgnoringModifiers); + } else if ([aNativeKeyEvent type] == NSFlagsChanged) { + aKeyEvent.mNativeKeyCode = [aNativeKeyEvent keyCode]; + aKeyEvent.mNativeModifierFlags = [aNativeKeyEvent modifierFlags]; + } + + aKeyEvent.mRefPoint = LayoutDeviceIntPoint(0, 0); + aKeyEvent.mIsChar = false; // XXX not used in XP level + + UInt32 kbType = GetKbdType(); + UInt32 nativeKeyCode = [aNativeKeyEvent keyCode]; + + aKeyEvent.mKeyCode = + ComputeGeckoKeyCode(nativeKeyCode, kbType, aKeyEvent.IsMeta()); + + switch (nativeKeyCode) { + case kVK_Command: + case kVK_Shift: + case kVK_Option: + case kVK_Control: + aKeyEvent.mLocation = nsIDOMKeyEvent::DOM_KEY_LOCATION_LEFT; + break; + + case kVK_RightCommand: + case kVK_RightShift: + case kVK_RightOption: + case kVK_RightControl: + aKeyEvent.mLocation = nsIDOMKeyEvent::DOM_KEY_LOCATION_RIGHT; + 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 = nsIDOMKeyEvent::DOM_KEY_LOCATION_NUMPAD; + break; + + default: + aKeyEvent.mLocation = nsIDOMKeyEvent::DOM_KEY_LOCATION_STANDARD; + break; + } + + aKeyEvent.mIsRepeat = + ([aNativeKeyEvent type] == NSKeyDown) ? [aNativeKeyEvent isARepeat] : false; + + MOZ_LOG(gLog, 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 (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()) { + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], + aKeyEvent.mKeyValue); + if (aKeyEvent.mKeyValue.IsEmpty() || + IsControlChar(aKeyEvent.mKeyValue[0])) { + NSUInteger cocoaState = + [aNativeKeyEvent modifierFlags] & ~NSControlKeyMask; + UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState); + aKeyEvent.mKeyValue = + TranslateToChar(nativeKeyCode, carbonState, kbType); + } + } + // 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); + MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING); + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +void +TISInputSourceWrapper::WillDispatchKeyboardEvent( + NSEvent* aNativeKeyEvent, + const nsAString* aInsertString, + WidgetKeyboardEvent& aKeyEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Nothing to do here if the native key event is neither NSKeyDown nor + // NSKeyUp because accessing [aNativeKeyEvent characters] causes throwing + // an exception. + if ([aNativeKeyEvent type] != NSKeyDown && + [aNativeKeyEvent type] != NSKeyUp) { + return; + } + + UInt32 kbType = GetKbdType(); + + if (MOZ_LOG_TEST(gLog, LogLevel::Info)) { + nsAutoString chars; + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars); + NS_ConvertUTF16toUTF8 utf8Chars(chars); + char16_t uniChar = static_cast<char16_t>(aKeyEvent.mCharCode); + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "aNativeKeyEvent=%p, [aNativeKeyEvent characters]=\"%s\", " + "aKeyEvent={ mMessage=%s, mCharCode=0x%X(%s) }, kbType=0x%X, " + "IsOpenedIMEMode()=%s", + this, aNativeKeyEvent, utf8Chars.get(), + GetGeckoKeyEventType(aKeyEvent), aKeyEvent.mCharCode, + uniChar ? NS_ConvertUTF16toUTF8(&uniChar, 1).get() : "", + 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 = + insertStringForCharCode.IsEmpty() ? 0 : insertStringForCharCode[0]; + aKeyEvent.SetCharCode(charCode); + // this is not a special key XXX not used in XP + aKeyEvent.mIsChar = (aKeyEvent.mMessage == eKeyPress); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "aKeyEvent.mKeyCode=0x%X, aKeyEvent.mCharCode=0x%X", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode)); + + 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] & NSAlphaShiftKeyMask) { + lockState |= alphaLock; + } + if ([aNativeKeyEvent modifierFlags] & NSNumericPadKeyMask) { + lockState |= kEventKeyModifierNumLockMask; + } + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "isRomanKeyboardLayout=%s, key=0x%X", + this, TrueOrFalse(isRomanKeyboardLayout), kbType, 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(gLog, 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(gLog, 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(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "hasCmdShiftOnlyChar=%s, originalCmdedShiftChar=U+%X", + this, TrueOrFalse(hasCmdShiftOnlyChar), originalCmdedShiftChar)); + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +uint32_t +TISInputSourceWrapper::ComputeGeckoKeyCode(UInt32 aNativeKeyCode, + UInt32 aKbType, + bool aCmdIsPressed) +{ + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, " + "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, " + "IsASCIICapable()=%s", + this, aNativeKeyCode, 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 this is ASCII capable, give up to compute it. + if (IsASCIICapable()) { + return 0; + } + + // Retry with ASCII capable keyboard layout. + TISInputSourceWrapper currentKeyboardLayout; + currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout(); + NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0); + keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType, + aCmdIsPressed); + + // However, if keyCode isn't for an alphabet keys or a numeric key, we should + // ignore it. For example, comma key of Thai layout is same as close-square- + // bracket key of US layout and an unicode character key of Thai layout is + // same as comma key of US layout. If we return NS_VK_COMMA for latter key, + // web application developers cannot distinguish with the former key. + return ((keyCode >= NS_VK_A && keyCode <= NS_VK_Z) || + (keyCode >= NS_VK_0 && keyCode <= NS_VK_9)) ? keyCode : 0; +} + +// 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) +{ + 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(gLog, LogLevel::Info)) { + CFArrayRef list = CreateAllKeyboardLayoutList(); + MOZ_LOG(gLog, 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(gLog, 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) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, ("")); + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, " + "type=%s, keyCode=%lld (0x%X), modifierFlags=0x%X, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\"", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + [aNativeEvent keyCode], [aNativeEvent keyCode], + [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] & NSCommandKeyMask)) { + [NSCursor setHiddenUntilMouseMoves:YES]; + } + + RefPtr<nsChildView> widget(mWidget); + + KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent); + AutoKeyEventStateCleaner remover(this); + + ComplexTextInputPanel* ctiPanel = ComplexTextInputPanel::GetSharedComplexTextInputPanel(); + if (ctiPanel && ctiPanel->IsInComposition()) { + nsAutoString committed; + ctiPanel->InterpretKeyEvent(aNativeEvent, committed); + if (!committed.IsEmpty()) { + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::HandleKeyDownEvent, " + "FAILED, due to BeginNativeInputTransaction() failure " + "at dispatching keydown for ComplexTextInputPanel", this)); + return false; + } + + WidgetKeyboardEvent imeEvent(true, eKeyDown, widget); + currentKeyEvent->InitKeyEvent(this, imeEvent); + imeEvent.mPluginTextEventString.Assign(committed); + nsEventStatus status = nsEventStatus_eIgnore; + mDispatcher->DispatchKeyboardEvent(eKeyDown, imeEvent, status, + currentKeyEvent); + } + return true; + } + + NSResponder* firstResponder = [[mView window] firstResponder]; + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::HandleKeyDownEvent, " + "FAILED, due to BeginNativeInputTransaction() failure " + "at dispatching keydown for ordinal cases", this)); + return false; + } + + WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget); + currentKeyEvent->InitKeyEvent(this, keydownEvent); + + nsEventStatus status = nsEventStatus_eIgnore; + mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, + currentKeyEvent); + currentKeyEvent->mKeyDownHandled = + (status == nsEventStatus_eConsumeNoDefault); + + if (Destroyed()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, " + "widget was destroyed by keydown event", this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + // The key down event may have shifted the focus, in which + // case we should not fire the key press. + // XXX This is a special code only on Cocoa widget, why is this needed? + if (firstResponder != [[mView window] firstResponder]) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, " + "view lost focus by keydown event", this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + if (currentKeyEvent->IsDefaultPrevented()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, " + "keydown event's default is prevented", this)); + return true; + } + + // 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 (!widget->IsPluginFocused() && (IsIMEEnabled() || IsASCIICapableOnly())) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, calling interpretKeyEvents", + this)); + [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]]; + interpretKeyEventsCalled = true; + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents", + this)); + } + + if (Destroyed()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed", + this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, wasComposing=%s, " + "IsIMEComposing()=%s", + this, TrueOrFalse(wasComposing), TrueOrFalse(IsIMEComposing()))); + + if (currentKeyEvent->CanDispatchKeyPressEvent() && + !wasComposing && !IsIMEComposing()) { + rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::HandleKeyDownEvent, " + "FAILED, due to BeginNativeInputTransaction() failure " + "at dispatching keypress", this)); + return false; + } + + WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget); + currentKeyEvent->InitKeyEvent(this, keypressEvent); + + // 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(keypressEvent))) { + currentKeyEvent->mKeyPressDispatched = + mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, + currentKeyEvent); + currentKeyEvent->mKeyPressHandled = + (status == nsEventStatus_eConsumeNoDefault); + currentKeyEvent->mKeyPressDispatched = true; + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, keypress event dispatched", + this)); + } + } + + // Note: mWidget might have become null here. Don't count on it from here on. + + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, ("")); + return currentKeyEvent->IsDefaultPrevented(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void +TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, " + "type=%s, keyCode=%lld (0x%X), modifierFlags=0x%X, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\", " + "IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + [aNativeEvent keyCode], [aNativeEvent keyCode], + [aNativeEvent modifierFlags], GetCharacters([aNativeEvent characters]), + GetCharacters([aNativeEvent charactersIgnoringModifiers]), + TrueOrFalse(IsIMEComposing()))); + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::HandleKeyUpEvent, " + "FAILED, due to BeginNativeInputTransaction() failure", this)); + return; + } + + WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget); + InitKeyEvent(aNativeEvent, keyupEvent); + + KeyEventState currentKeyEvent(aNativeEvent); + nsEventStatus status = nsEventStatus_eIgnore; + mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status, + ¤tKeyEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, " + "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08X, " + "sLastModifierState=0x%08X, IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode], + [aNativeEvent modifierFlags], sLastModifierState, + TrueOrFalse(IsIMEComposing()))); + + MOZ_ASSERT([aNativeEvent type] == NSFlagsChanged); + + 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 & ~NSDeviceIndependentModifierFlagsMask) != 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 NSFlagsChanged 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; + 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 & NSDeviceIndependentModifierFlagsMask) { + switch (flag) { + case NSAlphaShiftKeyMask: + keyCode = kVK_CapsLock; + dispatchKeyDown = true; + break; + + case NSNumericPadKeyMask: + // NSNumericPadKeyMask 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 NSHelpKeyMask: + keyCode = kVK_Help; + break; + + case NSFunctionKeyMask: + // An NSFunctionKeyMask 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 (NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask + // and NSCommandKeyMask) 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 NSShiftKeyMask: + keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift; + break; + case NSControlKeyMask: + keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control; + break; + case NSAlternateKeyMask: + keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option; + break; + case NSCommandKeyMask: + 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 NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask + // and NSCommandKeyMask 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 &= ~NSShiftKeyMask; + } + break; + } + case kVK_RightShift: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Shift); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSShiftKeyMask; + } + break; + } + case kVK_Command: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightCommand); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSCommandKeyMask; + } + break; + } + case kVK_RightCommand: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Command); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSCommandKeyMask; + } + break; + } + case kVK_Control: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightControl); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSControlKeyMask; + } + break; + } + case kVK_RightControl: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Control); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSControlKeyMask; + } + break; + } + case kVK_Option: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightOption); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSAlternateKeyMask; + } + break; + } + case kVK_RightOption: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Option); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSAlternateKeyMask; + } + break; + } + case kVK_Help: + modifiers &= ~NSHelpKeyMask; + break; + default: + break; + } + + NSEvent* event = + [NSEvent keyEventWithType:NSFlagsChanged + location:[aNativeEvent locationInWindow] + modifierFlags:modifiers + timestamp:[aNativeEvent timestamp] + windowNumber:[aNativeEvent windowNumber] + context:[aNativeEvent context] + 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_ABORT_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 & ~NSDeviceIndependentModifierFlagsMask)) { + return &((ModifierKey&)mModifierKeys[i]); + } + } + return nullptr; +} + +void +TextInputHandler::DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent, + bool aDispatchKeyDown) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + MOZ_LOG(gLog, 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] != NSFlagsChanged || IsIMEComposing()) { + return; + } + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchKeyEventForFlagsChanged, " + "FAILED, due to BeginNativeInputTransaction() failure", this)); + return; + } + + EventMessage message = aDispatchKeyDown ? eKeyDown : eKeyUp; + + // Fire a key event. + WidgetKeyboardEvent keyEvent(true, message, mWidget); + InitKeyEvent(aNativeEvent, keyEvent); + + // Attach a plugin event, in case keyEvent gets dispatched to a plugin. Only + // one field is needed -- the type. The other fields can be constructed as + // the need arises. But Gecko doesn't have anything equivalent to the + // NPCocoaEventFlagsChanged type, and this needs to be passed accurately to + // any plugin to which this event is sent. + NPCocoaEvent cocoaEvent; + nsCocoaUtils::InitNPCocoaEvent(&cocoaEvent); + cocoaEvent.type = NPCocoaEventFlagsChanged; + keyEvent.mPluginEvent.Copy(cocoaEvent); + + KeyEventState currentKeyEvent(aNativeEvent); + nsEventStatus status = nsEventStatus_eIgnore; + mDispatcher->DispatchKeyboardEvent(message, keyEvent, status, + ¤tKeyEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +TextInputHandler::InsertText(NSAttributedString* aAttrString, + NSRange* aReplacementRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::InsertText, aAttrString=\"%s\", " + "aReplacementRange=%p { location=%llu, length=%llu }, " + "IsIMEComposing()=%s, IgnoreIMEComposition()=%s, " + "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, " + "causedOtherKeyEvents=%s, compositionDispatched=%s", + this, GetCharacters([aAttrString string]), aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(IsIMEComposing()), TrueOrFalse(IgnoreIMEComposition()), + 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")); + + if (IgnoreIMEComposition()) { + return; + } + + InputContext context = mWidget->GetInputContext(); + bool isEditable = (context.mIMEState.mEnabled == IMEState::ENABLED || + context.mIMEState.mEnabled == IMEState::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; + } + // Delete the selected range. + RefPtr<TextInputHandler> kungFuDeathGrip(this); + 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; + } + + // 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::HandleKeyUpEvent, " + "FAILED, due to BeginNativeInputTransaction() failure", this)); + return; + } + + // 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. + keypressEvent.mIsChar = IsPrintableChar(str.CharAt(0)); + + // 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); + } 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. + } + + // Remove basic modifiers from keypress event because if they are included, + // nsPlaintextEditor ignores the event. + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | + MODIFIER_ALT | + MODIFIER_META); + + // 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_ABORT_BLOCK; +} + +bool +TextInputHandler::DoCommandBySelector(const char* aSelector) +{ + RefPtr<nsChildView> widget(mWidget); + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", " + "Destroyed()=%s, keydownHandled=%s, keypressHandled=%s, " + "causedOtherKeyEvents=%s", + this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()), + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A")); + + if (currentKeyEvent && currentKeyEvent->CanDispatchKeyPressEvent()) { + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::DoCommandBySelector, " + "FAILED, due to BeginNativeInputTransaction() failure " + "at dispatching keypress", this)); + return false; + } + + WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget); + currentKeyEvent->InitKeyEvent(this, keypressEvent); + + nsEventStatus status = nsEventStatus_eIgnore; + currentKeyEvent->mKeyPressDispatched = + mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, + currentKeyEvent); + currentKeyEvent->mKeyPressHandled = + (status == nsEventStatus_eConsumeNoDefault); + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::DoCommandBySelector, keypress event " + "dispatched, Destroyed()=%s, keypressHandled=%s", + this, TrueOrFalse(Destroyed()), + TrueOrFalse(currentKeyEvent->mKeyPressHandled))); + } + + return (!Destroyed() && currentKeyEvent && + currentKeyEvent->IsDefaultPrevented()); +} + + +#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); + } + + if (MOZ_LOG_TEST(gLog, 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(gLog, 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(gLog, LogLevel::Info)) { + CFArrayRef list = CreateAllIMEModeList(); + MOZ_LOG(gLog, 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; + tis.GetLocalizedName(name); + tis.GetInputSourceID(isid); + MOZ_LOG(gLog, LogLevel::Info, + (" %s\t<%s>%s%s\n", + NS_ConvertUTF16toUTF8(name).get(), + NS_ConvertUTF16toUTF8(isid).get(), + tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)", + tis.IsEnabled() ? "" : "\t(Isn't Enabled)")); + } + ::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. + * + ******************************************************************************/ + +NS_IMETHODIMP +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; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +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 (KeyboardLayoutOverrideRef().mOverrideEnabled) { + TISInputSourceWrapper tis; + tis.InitByLayoutID(KeyboardLayoutOverrideRef().mKeyboardLayout, true); + tis.WillDispatchKeyboardEvent(nativeEvent, insertString, aKeyboardEvent); + return; + } + TISInputSourceWrapper::CurrentInputSource(). + WillDispatchKeyboardEvent(nativeEvent, insertString, aKeyboardEvent); +} + +void +IMEInputHandler::NotifyIMEOfFocusChangeInGecko() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +void +IMEInputHandler::DiscardIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DiscardIMEComposition, " + "Destroyed()=%s, IsFocused()=%s, mView=%p, inputContext=%p", + this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()), + mView, mView ? [mView inputContext] : nullptr)); + + if (Destroyed()) { + return; + } + + if (!IsFocused()) { + // retry at next focus event + mPendingMethods |= kDiscardIMEComposition; + return; + } + + NS_ENSURE_TRUE_VOID(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + mIgnoreIMECommit = true; + [inputContext discardMarkedText]; + mIgnoreIMECommit = false; + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +void +IMEInputHandler::SyncASCIICapableOnly() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +void +IMEInputHandler::ResetTimer() +{ + NS_ASSERTION(mPendingMethods != 0, + "There are not pending methods, why this is called?"); + if (mTimer) { + mTimer->Cancel(); + } else { + mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_TRUE(mTimer, ); + } + mTimer->InitWithFuncCallback(FlushPendingMethods, this, 0, + nsITimer::TYPE_ONE_SHOT); +} + +void +IMEInputHandler::ExecutePendingMethods() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + if (![[NSApplication sharedApplication] isActive]) { + mIsInFocusProcessing = false; + // 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 & kDiscardIMEComposition) + DiscardIMEComposition(); + if (pendingMethods & kSyncASCIICapableOnly) + SyncASCIICapableOnly(); + if (pendingMethods & kNotifyIMEOfFocusChangeInGecko) { + NotifyIMEOfFocusChangeInGecko(); + } + + mIsInFocusProcessing = false; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +#pragma mark - + + +/****************************************************************************** + * + * IMEInputHandler implementation (native event handlers) + * + ******************************************************************************/ + +TextRangeType +IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle, + NSRange& aSelectedRange) +{ + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::ConvertToTextRangeType, " + "aUnderlineStyle=%llu, aSelectedRange.length=%llu,", + this, aUnderlineStyle, 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_ABORT_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(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%llu", + this, GetCharacters([aAttrString string]), count)); + + return count; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +already_AddRefed<mozilla::TextRangeArray> +IMEInputHandler::CreateTextRangeArray(NSAttributedString *aAttrString, + NSRange& aSelectedRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::CreateTextRangeArray, " + "range={ mStartOffset=%llu, mEndOffset=%llu, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::CreateTextRangeArray, " + "range={ mStartOffset=%llu, mEndOffset=%llu, mRangeType=%s }", + this, range.mStartOffset, range.mEndOffset, + ToChar(range.mRangeType))); + + return textRangeArray.forget(); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; +} + +bool +IMEInputHandler::DispatchCompositionStartEvent() +{ + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionStartEvent, " + "mSelectedRange={ location=%llu, length=%llu }, Destroyed()=%s, " + "mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s", + this, SelectedRange().location, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionStartEvent, " + "FAILED, due to BeginNativeInputTransaction() failure", this)); + return false; + } + + NS_ASSERTION(!mIsIMEComposing, "There is a composition already"); + mIsIMEComposing = true; + + nsEventStatus status; + rv = mDispatcher->StartComposition(status); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionStartEvent, " + "FAILED, due to StartComposition() failure", this)); + return false; + } + + if (Destroyed()) { + MOZ_LOG(gLog, 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_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionChangeEvent, " + "aText=\"%s\", aAttrString=\"%s\", " + "aSelectedRange={ location=%llu, length=%llu }, Destroyed()=%s, mView=%p, " + "mWidget=%p, inputContext=%p, mIsIMEComposing=%s", + this, NS_ConvertUTF16toUTF8(aText).get(), + GetCharacters([aAttrString string]), + aSelectedRange.location, 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(gLog, 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(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionChangeEvent, " + "FAILED, due to FlushPendingComposition() failure", this)); + return false; + } + + if (Destroyed()) { + MOZ_LOG(gLog, 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_ABORT_BLOCK_RETURN(false); +} + +bool +IMEInputHandler::DispatchCompositionCommitEvent(const nsAString* aCommitString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, 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(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionCommitEvent, " + "FAILED, due to BeginNativeInputTransaction() failure", this)); + } + } + } + + mIsIMEComposing = false; + mIMECompositionStart = UINT32_MAX; + if (mIMECompositionString) { + [mIMECompositionString release]; + mIMECompositionString = nullptr; + } + + if (Destroyed()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionCommitEvent, " + "destroyed by compositioncommit event", this)); + return false; + } + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void +IMEInputHandler::InsertTextAsCommittingComposition( + NSAttributedString* aAttrString, + NSRange* aReplacementRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "aAttrString=\"%s\", aReplacementRange=%p { location=%llu, length=%llu }, " + "Destroyed()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%llu, length=%llu }", + this, GetCharacters([aAttrString string]), aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), + mMarkedRange.location, mMarkedRange.length)); + + if (IgnoreIMECommit()) { + MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not" + "be called while canceling the composition"); + } + + 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)) { + if (!DispatchCompositionCommitEvent()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by commiting composition for setting replacement range", + this)); + return; + } + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + nsString str; + nsCocoaUtils::GetStringForNSString([aAttrString string], str); + + if (!IsIMEComposing()) { + // 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 (!DispatchCompositionStartEvent()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "cannot continue handling composition after compositionstart", this)); + return; + } + } + + if (!DispatchCompositionCommitEvent(&str)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by compositioncommit event", this)); + return; + } + + mMarkedRange = NSMakeRange(NSNotFound, 0); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString, + NSRange& aSelectedRange, + NSRange* aReplacementRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, " + "aAttrString=\"%s\", aSelectedRange={ location=%llu, length=%llu }, " + "aReplacementRange=%p { location=%llu, length=%llu }, " + "Destroyed()=%s, IgnoreIMEComposition()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%llu, length=%llu }, keyevent=%p, " + "keydownHandled=%s, keypressDispatched=%s, causedOtherKeyEvents=%s, " + "compositionDispatched=%s", + this, GetCharacters([aAttrString string]), + aSelectedRange.location, aSelectedRange.length, aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(Destroyed()), TrueOrFalse(IgnoreIMEComposition()), + TrueOrFalse(IsIMEComposing()), + mMarkedRange.location, mMarkedRange.length, + 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")); + + // 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; + } + + if (Destroyed() || IgnoreIMEComposition()) { + return; + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + // 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(gLog, 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)) { + NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange)); + } + + mMarkedRange.location = SelectedRange().location; + + if (!DispatchCompositionStartEvent()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, cannot continue handling " + "composition after dispatching compositionstart", this)); + return; + } + } + + if (!str.IsEmpty()) { + if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, " + "destroyed by compositioncommit event", this)); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NSAttributedString* +IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange, + NSRange* aActualRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "aRange={ location=%llu, length=%llu }, aActualRange=%p, Destroyed()=%s", + this, aRange.location, 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 && + mIMECompositionStart >= aRange.location && + mIMECompositionStart + compositionLength <= + aRange.location + aRange.length) { + 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(gLog, LogLevel::Info)) { + nsAutoString str; + nsCocoaUtils::GetStringForNSString(nsstr, str); + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "computed with mIMECompositionString (result string=\"%s\")", + this, NS_ConvertUTF16toUTF8(str).get())); + } + return result; + } + + nsAutoString str; + WidgetQueryContentEvent textContent(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; + } + textContent.InitForQueryTextContent(startOffset, aRange.length, options); + textContent.RequestFontRanges(); + DispatchEvent(textContent); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "textContent={ mSucceeded=%s, mReply={ mString=\"%s\", mOffset=%u } }", + this, TrueOrFalse(textContent.mSucceeded), + NS_ConvertUTF16toUTF8(textContent.mReply.mString).get(), + textContent.mReply.mOffset)); + + if (!textContent.mSucceeded) { + return nil; + } + + // We don't set vertical information at this point. If required, + // OS will calls drawsVerticallyForCharacterAtIndex. + NSMutableAttributedString* result = + nsCocoaUtils::GetNSMutableAttributedString(textContent.mReply.mString, + textContent.mReply.mFontRanges, + false, + mWidget->BackingScaleFactor()); + if (aActualRange) { + aActualRange->location = textContent.mReply.mOffset; + aActualRange->length = textContent.mReply.mString.Length(); + } + return result; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +bool +IMEInputHandler::HasMarkedText() +{ + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::HasMarkedText, " + "mMarkedRange={ location=%llu, length=%llu }", + this, mMarkedRange.location, mMarkedRange.length)); + + return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0); +} + +NSRange +IMEInputHandler::MarkedRange() +{ + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::MarkedRange, " + "mMarkedRange={ location=%llu, length=%llu }", + this, mMarkedRange.location, mMarkedRange.length)); + + if (!HasMarkedText()) { + return NSMakeRange(NSNotFound, 0); + } + return mMarkedRange; +} + +NSRange +IMEInputHandler::SelectedRange() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ " + "location=%llu, length=%llu }", + this, TrueOrFalse(Destroyed()), mSelectedRange.location, + mSelectedRange.length)); + + if (Destroyed()) { + return mSelectedRange; + } + + if (mSelectedRange.location != NSNotFound) { + MOZ_ASSERT(mIMEHasFocus); + return mSelectedRange; + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + WidgetQueryContentEvent selection(true, eQuerySelectedText, mWidget); + DispatchEvent(selection); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SelectedRange, selection={ mSucceeded=%s, " + "mReply={ mOffset=%u, mString.Length()=%u } }", + this, TrueOrFalse(selection.mSucceeded), selection.mReply.mOffset, + selection.mReply.mString.Length())); + + if (!selection.mSucceeded) { + return mSelectedRange; + } + + mWritingMode = selection.GetWritingMode(); + mRangeForWritingMode = NSMakeRange(selection.mReply.mOffset, + selection.mReply.mString.Length()); + + if (mIMEHasFocus) { + mSelectedRange = mRangeForWritingMode; + } + + return mRangeForWritingMode; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(mSelectedRange); +} + +bool +IMEInputHandler::DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK_RETURN(false); +} + +NSRect +IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange, + NSRange* aActualRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, " + "aRange={ location=%llu, length=%llu }, aActualRange=%p }", + this, TrueOrFalse(Destroyed()), aRange.location, 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 charRect(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; + } + charRect.InitForQueryTextRect(startOffset, 1, options); + DispatchEvent(charRect); + if (charRect.mSucceeded) { + r = charRect.mReply.mRect; + actualRange.location = charRect.mReply.mOffset; + actualRange.length = charRect.mReply.mString.Length(); + mWritingMode = charRect.GetWritingMode(); + mRangeForWritingMode = actualRange; + } else { + useCaretRect = true; + } + } + + if (useCaretRect) { + WidgetQueryContentEvent caretRect(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; + } + caretRect.InitForQueryCaretRect(startOffset, options); + DispatchEvent(caretRect); + if (!caretRect.mSucceeded) { + return rect; + } + r = caretRect.mReply.mRect; + r.width = 0; + actualRange.location = caretRect.mReply.mOffset; + 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::FirstRectForCharacterRange, " + "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, " + "actualRange={ location=%llu, length=%llu }", + this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y, + rect.size.width, rect.size.height, actualRange.location, + actualRange.length)); + + return rect; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSMakeRect(0.0, 0.0, 0.0, 0.0)); +} + +NSUInteger +IMEInputHandler::CharacterIndexForPoint(NSPoint& aPoint) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, 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 charAt(true, eQueryCharacterAtPoint, mWidget); + NSPoint ptInWindow = nsCocoaUtils::ConvertPointFromScreen(mainWindow, aPoint); + NSPoint ptInView = [mView convertPoint:ptInWindow fromView:nil]; + charAt.mRefPoint.x = + static_cast<int32_t>(ptInView.x) * mWidget->BackingScaleFactor(); + charAt.mRefPoint.y = + static_cast<int32_t>(ptInView.y) * mWidget->BackingScaleFactor(); + mWidget->DispatchWindowEvent(charAt); + if (!charAt.mSucceeded || + charAt.mReply.mOffset == WidgetQueryContentEvent::NOT_FOUND || + charAt.mReply.mOffset >= static_cast<uint32_t>(NSNotFound)) { + return NSNotFound; + } + + return charAt.mReply.mOffset; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSNotFound); +} + +extern "C" { +extern NSString *NSTextInputReplacementRangeAttributeName; +} + +NSArray* +IMEInputHandler::GetValidAttributesForMarkedText() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_LOG(gLog, 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_ABORT_BLOCK_NIL; +} + + +#pragma mark - + + +/****************************************************************************** + * + * IMEInputHandler implementation #2 + * + ******************************************************************************/ + +IMEInputHandler::IMEInputHandler(nsChildView* aWidget, + NSView<mozView> *aNativeView) + : TextInputHandlerBase(aWidget, aNativeView) + , mPendingMethods(0) + , mIMECompositionString(nullptr) + , mIMECompositionStart(UINT32_MAX) + , mIsIMEComposing(false) + , mIsIMEEnabled(true) + , mIsASCIICapableOnly(false) + , mIgnoreIMECommit(false) + , mIsInFocusProcessing(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(gLog, 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; + mIsInFocusProcessing = true; + + // 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(gLog, 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_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing), mWidget)); + + 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(gLog, 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_ABORT_BLOCK; +} + +void +IMEInputHandler::KillIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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()) { + return; + } + + if (IsFocused()) { + NS_ENSURE_TRUE_VOID(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + [inputContext discardMarkedText]; + return; + } + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::KillIMEComposition, Pending...", this)); + + // Commit the composition internally. + SendCommittedText(mIMECompositionString); + NS_ASSERTION(!mIsIMEComposing, "We're still in a composition"); + // The pending method will be fired by the next focus event. + mPendingMethods |= kDiscardIMEComposition; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::CommitIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!IsIMEComposing()) + return; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::CommitIMEComposition, mIMECompositionString=%s", + this, GetCharacters(mIMECompositionString))); + + 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_ABORT_BLOCK; +} + +void +IMEInputHandler::CancelIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!IsIMEComposing()) + return; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +bool +IMEInputHandler::IsFocused() +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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(gLog, LogLevel::Info, + ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this)); + + CFArrayRef langList = ::CFLocaleCopyPreferredLanguages(); + if (!langList) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info)) { + CFStringRef foundTIS; + tis.GetInputSourceID(foundTIS); + MOZ_LOG(gLog, 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_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::OnSelectionChange", this)); + + if (aIMENotification.mSelectionChangeData.mOffset == UINT32_MAX) { + 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; + } +} + +bool +IMEInputHandler::OnHandleEvent(NSEvent* aEvent) +{ + if (!IsFocused()) { + return false; + } + NSTextInputContext* inputContext = [mView inputContext]; + return [inputContext handleEvent:aEvent]; +} + +#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(gLog, 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, + 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, aInsertString); + return; + } + TISInputSourceWrapper::CurrentInputSource(). + InitKeyEvent(aNativeKeyEvent, aKeyEvent, aInsertString); +} + +nsresult +TextInputHandlerBase::SynthesizeNativeKeyEvent( + int32_t aNativeKeyboardLayout, + int32_t aNativeKeyCode, + uint32_t aModifierFlags, + const nsAString& aCharacters, + const nsAString& aUnmodifiedCharacters) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + static const uint32_t sModifierFlagMap[][2] = { + { nsIWidget::CAPS_LOCK, NSAlphaShiftKeyMask }, + { nsIWidget::SHIFT_L, NSShiftKeyMask | 0x0002 }, + { nsIWidget::SHIFT_R, NSShiftKeyMask | 0x0004 }, + { nsIWidget::CTRL_L, NSControlKeyMask | 0x0001 }, + { nsIWidget::CTRL_R, NSControlKeyMask | 0x2000 }, + { nsIWidget::ALT_L, NSAlternateKeyMask | 0x0020 }, + { nsIWidget::ALT_R, NSAlternateKeyMask | 0x0040 }, + { nsIWidget::COMMAND_L, NSCommandKeyMask | 0x0008 }, + { nsIWidget::COMMAND_R, NSCommandKeyMask | 0x0010 }, + { nsIWidget::NUMERIC_KEY_PAD, NSNumericPadKeyMask }, + { nsIWidget::HELP, NSHelpKeyMask }, + { nsIWidget::FUNCTION, NSFunctionKeyMask } + }; + + uint32_t modifierFlags = 0; + for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) { + if (aModifierFlags & sModifierFlagMap[i][0]) { + modifierFlags |= sModifierFlagMap[i][1]; + } + } + + NSInteger windowNumber = [[mView window] windowNumber]; + bool sendFlagsChangedEvent = IsModifierKey(aNativeKeyCode); + NSEventType eventType = sendFlagsChangedEvent ? NSFlagsChanged : NSKeyDown; + NSEvent* downEvent = + [NSEvent keyEventWithType:eventType + location:NSMakePoint(0,0) + modifierFlags:modifierFlags + timestamp:0 + windowNumber:windowNumber + context:[NSGraphicsContext currentContext] + characters:nsCocoaUtils::ToNSString(aCharacters) + charactersIgnoringModifiers:nsCocoaUtils::ToNSString(aUnmodifiedCharacters) + isARepeat:NO + keyCode:aNativeKeyCode]; + + NSEvent* upEvent = sendFlagsChangedEvent ? + nil : nsCocoaUtils::MakeNewCocoaEventWithType(NSKeyUp, 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_ABORT_BLOCK_NSRESULT; +} + +NSInteger +TextInputHandlerBase::GetWindowLevel() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%X)", + this, GetWindowLevelName(windowLevel), windowLevel)); + + return windowLevel; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSNormalWindowLevel); +} + +NS_IMETHODIMP +TextInputHandlerBase::AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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(gLog, LogLevel::Info, + ("%p TextInputHandlerBase::AttachNativeKeyEvent, key=0x%X, char=0x%X, " + "mod=0x%X", this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, + aKeyEvent.mModifiers)); + + NSEventType eventType; + if (aKeyEvent.mMessage == eKeyUp) { + eventType = NSKeyUp; + } else { + eventType = NSKeyDown; + } + + static const uint32_t sModifierFlagMap[][2] = { + { MODIFIER_SHIFT, NSShiftKeyMask }, + { MODIFIER_CONTROL, NSControlKeyMask }, + { MODIFIER_ALT, NSAlternateKeyMask }, + { MODIFIER_ALTGRAPH, NSAlternateKeyMask }, + { MODIFIER_META, NSCommandKeyMask }, + { MODIFIER_CAPSLOCK, NSAlphaShiftKeyMask }, + { MODIFIER_NUMLOCK, NSNumericPadKeyMask } + }; + + NSUInteger modifierFlags = 0; + for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) { + if (aKeyEvent.mModifiers & sModifierFlagMap[i][0]) { + modifierFlags |= sModifierFlagMap[i][1]; + } + } + + NSInteger windowNumber = [[mView window] windowNumber]; + + 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]; + } + + aKeyEvent.mNativeKeyEvent = + [NSEvent keyEventWithType:eventType + location:NSMakePoint(0,0) + modifierFlags:modifierFlags + timestamp:0 + windowNumber:windowNumber + context:[NSGraphicsContext currentContext] + characters:characters + charactersIgnoringModifiers:characters + isARepeat:NO + keyCode:0]; // Native key code not currently needed + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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( + const WidgetKeyboardEvent& aKeyEvent) +{ + // this is not character inputting event, simply. + if (aKeyEvent.mNativeCharacters.IsEmpty() || + aKeyEvent.IsMeta()) { + return false; + } + return !IsControlChar(aKeyEvent.mNativeCharacters[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() +{ + NS_ASSERTION(!!sSecureEventInputCount == !!::IsSecureEventInputEnabled(), + "Some other process has enabled secure event input"); + return !!sSecureEventInputCount; +} + +/* static */ void +TextInputHandlerBase::EnsureSecureEventInputDisabled() +{ + while (sSecureEventInputCount) { + TextInputHandlerBase::DisableSecureEventInput(); + } +} + +#pragma mark - + + +/****************************************************************************** + * + * TextInputHandlerBase::KeyEventState implementation + * + ******************************************************************************/ + +void +TextInputHandlerBase::KeyEventState::InitKeyEvent( + TextInputHandlerBase* aHandler, + WidgetKeyboardEvent& aKeyEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_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:[mKeyEvent context] + characters:unhandledNSString + charactersIgnoringModifiers:[mKeyEvent charactersIgnoringModifiers] + isARepeat:[mKeyEvent isARepeat] + keyCode:[mKeyEvent keyCode]]; + } + + aHandler->InitKeyEvent(nativeEvent, aKeyEvent, mInsertString); + + NS_OBJC_END_TRY_ABORT_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; + } +} diff --git a/widget/cocoa/VibrancyManager.h b/widget/cocoa/VibrancyManager.h new file mode 100644 index 000000000..7a7ea3af1 --- /dev/null +++ b/widget/cocoa/VibrancyManager.h @@ -0,0 +1,120 @@ +/* -*- 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 { + LIGHT, + DARK, + TOOLTIP, + MENU, + HIGHLIGHTED_MENUITEM, + SHEET, + 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) + { + MOZ_ASSERT(SystemSupportsVibrancy(), + "Don't instantiate this if !SystemSupportsVibrancy()"); + } + + /** + * 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. + */ + void UpdateVibrantRegion(VibrancyType aType, + const LayoutDeviceIntRegion& aRegion); + + bool HasVibrantRegions() { return !mVibrantRegions.IsEmpty(); } + + /** + * Clear the vibrant areas that we know about. + * The clearing happens in the current NSGraphicsContext. If you call this + * from within an -[NSView drawRect:] implementation, the currrent + * NSGraphicsContext is already correctly set to the window drawing context. + */ + void ClearVibrantAreas() const; + + /** + * Return the fill color that should be drawn on top of the cleared window + * parts. Usually this would be drawn by -[NSVisualEffectView drawRect:]. + * The returned color is opaque if the system-wide "Reduce transparency" + * preference is set. + */ + NSColor* VibrancyFillColorForType(VibrancyType aType); + + /** + * Return the font smoothing background color that should be used for text + * drawn on top of the vibrant window parts. + */ + NSColor* VibrancyFontSmoothingBackgroundColorForType(VibrancyType aType); + + /** + * Check whether the operating system supports vibrancy at all. + * You may only create a VibrancyManager instance if this returns true. + * @return Whether VibrancyManager can be used on this OS. + */ + static bool SystemSupportsVibrancy(); + +protected: + void ClearVibrantRegion(const LayoutDeviceIntRegion& aVibrantRegion) const; + NSView* CreateEffectView(VibrancyType aType); + + 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 000000000..b6176de2b --- /dev/null +++ b/widget/cocoa/VibrancyManager.mm @@ -0,0 +1,271 @@ +/* -*- 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" +#include "nsChildView.h" +#import <objc/message.h> + +using namespace mozilla; + +void +VibrancyManager::UpdateVibrantRegion(VibrancyType aType, + const LayoutDeviceIntRegion& aRegion) +{ + if (aRegion.IsEmpty()) { + mVibrantRegions.Remove(uint32_t(aType)); + return; + } + auto& vr = *mVibrantRegions.LookupOrAdd(uint32_t(aType)); + vr.UpdateRegion(aRegion, mCoordinateConverter, mContainerView, ^() { + return this->CreateEffectView(aType); + }); +} + +void +VibrancyManager::ClearVibrantAreas() const +{ + for (auto iter = mVibrantRegions.ConstIter(); !iter.Done(); iter.Next()) { + ClearVibrantRegion(iter.UserData()->Region()); + } +} + +void +VibrancyManager::ClearVibrantRegion(const LayoutDeviceIntRegion& aVibrantRegion) const +{ + [[NSColor clearColor] set]; + + for (auto iter = aVibrantRegion.RectIter(); !iter.Done(); iter.Next()) { + NSRectFill(mCoordinateConverter.DevPixelsToCocoaPoints(iter.Get())); + } +} + +@interface NSView(CurrentFillColor) +- (NSColor*)_currentFillColor; +@end + +static NSColor* +AdjustedColor(NSColor* aFillColor, VibrancyType aType) +{ + if (aType == VibrancyType::MENU && [aFillColor alphaComponent] == 1.0) { + // The opaque fill color that's used for the menu background when "Reduce + // vibrancy" is checked in the system accessibility prefs is too dark. + // This is probably because we're not using the right material for menus, + // see VibrancyManager::CreateEffectView. + return [NSColor colorWithDeviceWhite:0.96 alpha:1.0]; + } + return aFillColor; +} + +NSColor* +VibrancyManager::VibrancyFillColorForType(VibrancyType aType) +{ + NSView* view = mVibrantRegions.LookupOrAdd(uint32_t(aType))->GetAnyView(); + + if (view && [view respondsToSelector:@selector(_currentFillColor)]) { + // -[NSVisualEffectView _currentFillColor] is the color that our view + // would draw during its drawRect implementation, if we hadn't + // disabled that. + return AdjustedColor([view _currentFillColor], aType); + } + return [NSColor whiteColor]; +} + +@interface NSView(FontSmoothingBackgroundColor) +- (NSColor*)fontSmoothingBackgroundColor; +@end + +NSColor* +VibrancyManager::VibrancyFontSmoothingBackgroundColorForType(VibrancyType aType) +{ + NSView* view = mVibrantRegions.LookupOrAdd(uint32_t(aType))->GetAnyView(); + + if (view && [view respondsToSelector:@selector(fontSmoothingBackgroundColor)]) { + return [view fontSmoothingBackgroundColor]; + } + return [NSColor clearColor]; +} + +static void +DrawRectNothing(id self, SEL _cmd, NSRect aRect) +{ + // The super implementation would clear the background. + // That's fine for views that are placed below their content, but our + // setup is different: Our drawn content is drawn to mContainerView, which + // sits below this EffectView. So we must not clear the background here, + // because we'd erase that drawn content. + // Of course the regular content drawing still needs to clear the background + // behind vibrant areas. This is taken care of by having nsNativeThemeCocoa + // return true from NeedToClearBackgroundBehindWidget for vibrant widgets. +} + +static NSView* +HitTestNil(id self, SEL _cmd, NSPoint aPoint) +{ + // This view must be transparent to mouse events. + return nil; +} + +static BOOL +AllowsVibrancyYes(id self, SEL _cmd) +{ + // Means that the foreground is blended using a vibrant blend mode. + return YES; +} + +static Class +CreateEffectViewClass(BOOL aForegroundVibrancy) +{ + // Create a class called EffectView that inherits from NSVisualEffectView + // and overrides the methods -[NSVisualEffectView drawRect:] and + // -[NSView hitTest:]. + Class NSVisualEffectViewClass = NSClassFromString(@"NSVisualEffectView"); + const char* className = aForegroundVibrancy + ? "EffectViewWithForegroundVibrancy" : "EffectViewWithoutForegroundVibrancy"; + Class EffectViewClass = objc_allocateClassPair(NSVisualEffectViewClass, className, 0); + class_addMethod(EffectViewClass, @selector(drawRect:), (IMP)DrawRectNothing, + "v@:{CGRect={CGPoint=dd}{CGSize=dd}}"); + class_addMethod(EffectViewClass, @selector(hitTest:), (IMP)HitTestNil, + "@@:{CGPoint=dd}"); + if (aForegroundVibrancy) { + // Also override the -[NSView allowsVibrancy] method to return YES. + class_addMethod(EffectViewClass, @selector(allowsVibrancy), (IMP)AllowsVibrancyYes, "I@:"); + } + return EffectViewClass; +} + +static id +AppearanceForVibrancyType(VibrancyType aType) +{ + Class NSAppearanceClass = NSClassFromString(@"NSAppearance"); + switch (aType) { + case VibrancyType::LIGHT: + case VibrancyType::TOOLTIP: + case VibrancyType::MENU: + case VibrancyType::HIGHLIGHTED_MENUITEM: + case VibrancyType::SHEET: + case VibrancyType::SOURCE_LIST: + case VibrancyType::SOURCE_LIST_SELECTION: + case VibrancyType::ACTIVE_SOURCE_LIST_SELECTION: + return [NSAppearanceClass performSelector:@selector(appearanceNamed:) + withObject:@"NSAppearanceNameVibrantLight"]; + case VibrancyType::DARK: + return [NSAppearanceClass performSelector:@selector(appearanceNamed:) + withObject:@"NSAppearanceNameVibrantDark"]; + } +} + +#if !defined(MAC_OS_X_VERSION_10_10) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10 +enum { + NSVisualEffectStateFollowsWindowActiveState, + NSVisualEffectStateActive, + NSVisualEffectStateInactive +}; + +enum { + NSVisualEffectMaterialTitlebar = 3 +}; +#endif + +#if !defined(MAC_OS_X_VERSION_10_11) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_11 +enum { + NSVisualEffectMaterialMenu = 5, + NSVisualEffectMaterialSidebar = 7 +}; +#endif + +static NSUInteger +VisualEffectStateForVibrancyType(VibrancyType aType) +{ + switch (aType) { + case VibrancyType::TOOLTIP: + case VibrancyType::MENU: + case VibrancyType::HIGHLIGHTED_MENUITEM: + case VibrancyType::SHEET: + // Tooltip and menu windows are never "key" and sheets always looks + // active, so we need to tell the vibrancy effect to look active + // regardless of window state. + return NSVisualEffectStateActive; + default: + return NSVisualEffectStateFollowsWindowActiveState; + } +} + +static BOOL +HasVibrantForeground(VibrancyType aType) +{ + switch (aType) { + case VibrancyType::MENU: + return YES; + default: + return NO; + } +} + +#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 +enum { + NSVisualEffectMaterialSelection = 4 +}; +#endif + +@interface NSView(NSVisualEffectViewMethods) +- (void)setState:(NSUInteger)state; +- (void)setMaterial:(NSUInteger)material; +- (void)setEmphasized:(BOOL)emphasized; +@end + +NSView* +VibrancyManager::CreateEffectView(VibrancyType aType) +{ + static Class EffectViewClassWithoutForegroundVibrancy = CreateEffectViewClass(NO); + static Class EffectViewClassWithForegroundVibrancy = CreateEffectViewClass(YES); + + Class EffectViewClass = HasVibrantForeground(aType) + ? EffectViewClassWithForegroundVibrancy : EffectViewClassWithoutForegroundVibrancy; + NSView* effectView = [[EffectViewClass alloc] initWithFrame:NSZeroRect]; + [effectView performSelector:@selector(setAppearance:) + withObject:AppearanceForVibrancyType(aType)]; + [effectView setState:VisualEffectStateForVibrancyType(aType)]; + + BOOL canUseElCapitanMaterials = nsCocoaFeatures::OnElCapitanOrLater(); + if (aType == VibrancyType::MENU) { + // Before 10.11 there is no material that perfectly matches the menu + // look. Of all available material types, NSVisualEffectMaterialTitlebar + // is the one that comes closest. + [effectView setMaterial:canUseElCapitanMaterials ? NSVisualEffectMaterialMenu + : NSVisualEffectMaterialTitlebar]; + } else if (aType == VibrancyType::SOURCE_LIST && canUseElCapitanMaterials) { + [effectView setMaterial:NSVisualEffectMaterialSidebar]; + } else if (aType == VibrancyType::HIGHLIGHTED_MENUITEM || + aType == VibrancyType::SOURCE_LIST_SELECTION || + aType == VibrancyType::ACTIVE_SOURCE_LIST_SELECTION) { + [effectView setMaterial:NSVisualEffectMaterialSelection]; + if ([effectView respondsToSelector:@selector(setEmphasized:)] && + aType != VibrancyType::SOURCE_LIST_SELECTION) { + [effectView setEmphasized:YES]; + } + } + + return effectView; +} + +static bool +ComputeSystemSupportsVibrancy() +{ +#ifdef __x86_64__ + return NSClassFromString(@"NSAppearance") && + NSClassFromString(@"NSVisualEffectView"); +#else + // objc_allocateClassPair doesn't work in 32 bit mode, so turn off vibrancy. + return false; +#endif +} + +/* static */ bool +VibrancyManager::SystemSupportsVibrancy() +{ + static bool supportsVibrancy = ComputeSystemSupportsVibrancy(); + return supportsVibrancy; +} diff --git a/widget/cocoa/ViewRegion.h b/widget/cocoa/ViewRegion.h new file mode 100644 index 000000000..a8efccaff --- /dev/null +++ b/widget/cocoa/ViewRegion.h @@ -0,0 +1,53 @@ +/* -*- 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 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] : nil; } + +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 000000000..b3605caa2 --- /dev/null +++ b/widget/cocoa/ViewRegion.mm @@ -0,0 +1,71 @@ +/* -*- 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> + +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; + mViews.SwapElements(viewsToRecycle); + // 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 000000000..7023a17ba --- /dev/null +++ b/widget/cocoa/WidgetTraceEvent.mm @@ -0,0 +1,85 @@ +/* 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:NSApplicationDefined + 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/crashtests/373122-1-inner.html b/widget/cocoa/crashtests/373122-1-inner.html new file mode 100644 index 000000000..5c14166b7 --- /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 000000000..a57e5f424 --- /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 000000000..554b2dac7 --- /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 000000000..800eaa355 --- /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 000000000..fe6e4532b --- /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 000000000..1bbc27ba0 --- /dev/null +++ b/widget/cocoa/crashtests/435223-1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body> +<div style="min-width: -moz-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.xul b/widget/cocoa/crashtests/444260-1.xul new file mode 100644 index 000000000..f1a84023d --- /dev/null +++ b/widget/cocoa/crashtests/444260-1.xul @@ -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 000000000..f8bac76e6 --- /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 000000000..449459180 --- /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 000000000..cc9b9700c --- /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 000000000..cab7e7eb3 --- /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 000000000..d25d92315 --- /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 000000000..b65fe0139 --- /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 444260-1.xul +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 000000000..5ca8ec5ac --- /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 000000000..d00e87636 --- /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 000000000..9b2d19e0f --- /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 000000000..5d011c1fd --- /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 000000000..5284eaec5 --- /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 000000000..5e6738cff --- /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 000000000..4e3e19e22 --- /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 000000000..6a92cf680 --- /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 000000000..5e5416b4e --- /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 000000000..0ac53a973 --- /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 000000000..1360f8227 --- /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 000000000..ad146e486 --- /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 000000000..4c16bb8bd --- /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 000000000..b48f03ae0 --- /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 000000000..f62c04657 --- /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 000000000..98d19e9ef --- /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 000000000..0a077fa67 --- /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 000000000..31bca3c90 --- /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 000000000..0419be0af --- /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 000000000..e48fd0cb3 --- /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 000000000..8f5faee5f --- /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 000000000..3a80e7ce9 --- /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 000000000..0574a584c --- /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 000000000..9a0a276c3 --- /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 000000000..6a1948f52 --- /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 000000000..7d637f4be --- /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 000000000..5dd054dd4 --- /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 000000000..5ac63c25c --- /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 000000000..ee7528c59 --- /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 000000000..41c47af11 --- /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 000000000..275bf1c69 --- /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 000000000..fdd3f8e71 --- /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 000000000..19d2d8912 --- /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 000000000..0ed46ce75 --- /dev/null +++ b/widget/cocoa/cursors/zoomOut@2x.png diff --git a/widget/cocoa/moz.build b/widget/cocoa/moz.build new file mode 100644 index 000000000..7c995d900 --- /dev/null +++ b/widget/cocoa/moz.build @@ -0,0 +1,141 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsPIWidgetCocoa.idl', +] + +XPIDL_MODULE = 'widget_cocoa' + +EXPORTS += [ + 'mozView.h', + 'nsBidiKeyboard.h', + 'nsChangeObserver.h', + 'nsCocoaDebugUtils.h', + 'nsCocoaFeatures.h', + 'nsCocoaUtils.h', +] + +UNIFIED_SOURCES += [ + 'ComplexTextInputPanel.mm', + 'GfxInfo.mm', + 'NativeKeyBindings.mm', + 'nsAppShell.mm', + 'nsBidiKeyboard.mm', + 'nsCocoaFeatures.mm', + 'nsCocoaUtils.mm', + 'nsCocoaWindow.mm', + 'nsColorPicker.mm', + 'nsCursorManager.mm', + 'nsDeviceContextSpecX.mm', + 'nsFilePicker.mm', + 'nsIdleServiceX.mm', + 'nsLookAndFeel.mm', + 'nsMacCursor.mm', + 'nsMacDockSupport.mm', + 'nsMacWebAppUtils.mm', + 'nsMenuBarX.mm', + 'nsMenuGroupOwnerX.mm', + 'nsMenuItemIconX.mm', + 'nsMenuItemX.mm', + 'nsMenuUtilsX.mm', + 'nsMenuX.mm', + 'nsPrintDialogX.mm', + 'nsPrintOptionsX.mm', + 'nsPrintSettingsX.mm', + 'nsScreenCocoa.mm', + 'nsScreenManagerCocoa.mm', + 'nsSound.mm', + 'nsStandaloneNativeMenu.mm', + 'nsSystemStatusBarCocoa.mm', + 'nsToolkit.mm', + 'nsWidgetFactory.mm', + 'nsWindowMap.mm', + 'OSXNotificationCenter.mm', + 'RectTextureImage.mm', + 'SwipeTracker.mm', + 'TextInputHandler.mm', + 'VibrancyManager.mm', + 'ViewRegion.mm', + 'WidgetTraceEvent.mm', +] + +# These files cannot be built in unified mode because they cause symbol conflicts +SOURCES += [ + 'nsChildView.mm', + 'nsClipboard.mm', + 'nsCocoaDebugUtils.mm', + 'nsDragService.mm', + 'nsNativeThemeCocoa.mm', +] + +if not CONFIG['RELEASE_OR_BETA'] or CONFIG['MOZ_DEBUG']: + SOURCES += [ + 'nsSandboxViolationSink.mm', + ] + +include('/ipc/chromium/chromium-config.mozbuild') + +# XXX: We should fix these warnings. +ALLOW_COMPILER_WARNINGS = True + +FINAL_LIBRARY = 'xul' +LOCAL_INCLUDES += [ + '/layout/forms', + '/layout/generic', + '/layout/style', + '/layout/xul', + '/widget', +] + +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', +]) + +CXXFLAGS += CONFIG['TK_CFLAGS'] diff --git a/widget/cocoa/mozView.h b/widget/cocoa/mozView.h new file mode 100644 index 000000000..9e94e3ab4 --- /dev/null +++ b/widget/cocoa/mozView.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 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 listing all the methods that an object which wants +// to live in gecko's widget hierarchy must implement. |nsChildView| +// makes assumptions that any NSView with which it comes in contact will +// implement this protocol. +@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; + + // return a context menu for this view +- (NSMenu*)contextMenu; + + // Allows callers to do a delayed invalidate (e.g., if an invalidate + // happens during drawing) +- (void)setNeedsPendingDisplay; +- (void)setNeedsPendingDisplayInRect:(NSRect)invalidRect; + + // 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 000000000..b7836b639 --- /dev/null +++ b/widget/cocoa/nsAppShell.h @@ -0,0 +1,71 @@ +/* -*- 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_ + +#include "nsBaseAppShell.h" +#include "nsTArray.h" + +// GeckoNSApplication +// +// Subclass of NSApplication for filtering out certain events. +@interface GeckoNSApplication : NSApplication +{ +} +@end + +@class AppShellDelegate; + +class nsAppShell : public nsBaseAppShell +{ +public: + NS_IMETHOD ResumeNative(void); + + nsAppShell(); + + nsresult Init(); + + NS_IMETHOD Run(void); + NS_IMETHOD Exit(void); + NS_IMETHOD OnProcessNextEvent(nsIThreadInternal *aThread, bool aMayWait); + NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal *aThread, + bool aEventWasProcessed); + + // public only to be visible to Objective-C code that must call it + void WillTerminate(); + +protected: + virtual ~nsAppShell(); + + virtual void ScheduleNativeEventCallback(); + virtual bool ProcessNextNativeEvent(bool aMayWait); + + static void ProcessGeckoEvents(void* aInfo); + +protected: + CFMutableArrayRef mAutoreleasePools; + + AppShellDelegate* mDelegate; + CFRunLoopRef mCFRunLoop; + CFRunLoopSourceRef mCFRunLoopSource; + + 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 000000000..33ce8e742 --- /dev/null +++ b/widget/cocoa/nsAppShell.mm @@ -0,0 +1,907 @@ +/* -*- 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. + */ + +#import <Cocoa/Cocoa.h> + +#include "CustomCocoaEvents.h" +#include "mozilla/WidgetTraceEvent.h" +#include "nsAppShell.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsString.h" +#include "nsIRollupListener.h" +#include "nsIWidget.h" +#include "nsThreadUtils.h" +#include "nsIWindowMediator.h" +#include "nsServiceManagerUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsIWebBrowserChrome.h" +#include "nsObjCExceptions.h" +#include "nsCocoaFeatures.h" +#include "nsCocoaUtils.h" +#include "nsChildView.h" +#include "nsToolkit.h" +#include "TextInputHandler.h" +#include "mozilla/HangMonitor.h" +#include "GeckoProfiler.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" + +using namespace mozilla::widget; + +// 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 mAssertionID = kIOPMNullAssertionID; + + NS_IMETHOD Callback(const nsAString& aTopic, const nsAString& aState) override { + if (!aTopic.EqualsASCII("screen")) { + return NS_OK; + } + // 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")) { + // Prevent screen saver. + CFStringRef cf_topic = + ::CFStringCreateWithCharacters(kCFAllocatorDefault, + reinterpret_cast<const UniChar*> + (aTopic.Data()), + aTopic.Length()); + IOReturn success = + ::IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, + cf_topic, + &mAssertionID); + CFRelease(cf_topic); + if (success != kIOReturnSuccess) { + NS_WARNING("failed to disable screensaver"); + } + } else { + // Re-enable screen saver. + NS_WARNING("Releasing screensaver"); + if (mAssertionID != kIOPMNullAssertionID) { + IOReturn result = ::IOPMAssertionRelease(mAssertionID); + if (result != kIOReturnSuccess) { + NS_WARNING("failed to release screensaver"); + } + } + } + return NS_OK; + } +}; // MacWakeLockListener + +// defined in nsCocoaWindow.mm +extern int32_t gXULModalLevel; + +static bool gAppShellMethodsSwizzled = false; + +@implementation GeckoNSApplication + +- (void)sendEvent:(NSEvent *)anEvent +{ + mozilla::HangMonitor::NotifyActivity(); + if ([anEvent type] == NSApplicationDefined && + [anEvent subtype] == kEventSubtypeTrace) { + mozilla::SignalTracerThread(); + return; + } + [super sendEvent:anEvent]; +} + +#if defined(MAC_OS_X_VERSION_10_12) && \ + MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12 && \ + __LP64__ +// 10.12 changed `mask` to NSEventMask (unsigned long long) for x86_64 builds. +- (NSEvent*)nextEventMatchingMask:(NSEventMask)mask +#else +- (NSEvent*)nextEventMatchingMask:(NSUInteger)mask +#endif + untilDate:(NSDate*)expiration + inMode:(NSString*)mode + dequeue:(BOOL)flag +{ + if (expiration) { + mozilla::HangMonitor::Suspend(); + } + NSEvent* nextEvent = [super nextEventMatchingMask:mask + untilDate:expiration inMode:mode dequeue:flag]; + if (expiration) { + mozilla::HangMonitor::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; +- (void)beginMenuTracking:(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_ABORT_BLOCK; + + if (mCFRunLoop) { + if (mCFRunLoopSource) { + ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource, + kCFRunLoopCommonModes); + ::CFRelease(mCFRunLoopSource); + } + ::CFRelease(mCFRunLoop); + } + + if (mAutoreleasePools) { + NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0, + "nsAppShell destroyed without popping all autorelease pools"); + ::CFRelease(mAutoreleasePools); + } + + [mDelegate release]; + + NS_OBJC_END_TRY_ABORT_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; + } +} + +// An undocumented CoreGraphics framework method, present in the same form +// since at least OS X 10.5. +extern "C" CGError CGSSetDebugOptions(int options); + +// 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_ABORT_BLOCK_NSRESULT; + + // No event loop is running yet (unless an embedding app that uses + // NSApplicationMain() is running). + NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init]; + + // 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); + + // Get the path of the nib file, which lives in the GRE location + nsCOMPtr<nsIFile> nibFile; + nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(nibFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nibFile->AppendNative(NS_LITERAL_CSTRING("res")); + nibFile->AppendNative(NS_LITERAL_CSTRING("MainMenu.nib")); + + nsAutoCString nibPath; + rv = nibFile->GetNativePath(nibPath); + NS_ENSURE_SUCCESS(rv, rv); + + // 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 loadNibFile: + [NSString stringWithUTF8String:(const char*)nibPath.get()] + externalNameTable: + [NSDictionary dictionaryWithObject:[GeckoNSApplication sharedApplication] + forKey:@"NSOwner"] + withZone:NSDefaultMallocZone()]; + + 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); + + rv = nsBaseAppShell::Init(); + + if (!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 (nsCocoaFeatures::OnYosemiteOrLater()) { + // Explicitly turn off CGEvent logging. This works around bug 1092855. + // If there are already CGEvents in the log, turning off logging also + // causes those events to be written to disk. But at this point no + // CGEvents have yet been processed. CGEvents are events (usually + // input events) pulled from the WindowServer. An option of 0x80000008 + // turns on CGEvent logging. + CGSSetDebugOptions(0x80000007); + } + +#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_ABORT_BLOCK_NSRESULT; +} + +// 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_ABORT_BLOCK; + PROFILER_LABEL("Events", "ProcessGeckoEvents", + js::ProfileEntry::Category::EVENTS); + + 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:NSApplicationDefined + 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; + } + + // Still needed to avoid crashes on quit in most Mochitests. + [NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined + 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_ABORT_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_ABORT_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_ABORT_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_ABORT_BLOCK; + + bool eventProcessed = false; + NSString* currentMode = nil; + + if (mTerminated) + return false; + + bool wasRunningEventLoop = mRunningEventLoop; + mRunningEventLoop = aMayWait; + NSDate* waitUntil = nil; + if (aMayWait) + waitUntil = [NSDate distantFuture]; + + NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop]; + + EventQueueRef currentEventQueue = GetCurrentEventQueue(); + EventTargetRef eventDispatcherTarget = GetEventDispatcherTarget(); + + if (aMayWait) { + mozilla::HangMonitor::Suspend(); + } + + // 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:NSAnyEventMask + untilDate:waitUntil + inMode:currentMode + dequeue:YES]; + if (nextEvent) { + mozilla::HangMonitor::NotifyActivity(); + [NSApp sendEvent:nextEvent]; + eventProcessed = true; + } + } 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 != NSApplicationDefined))); + // 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); + SendEventToEventTarget(currentEvent, eventDispatcherTarget); + // This call to ReleaseEvent() matches a call to RetainEvent() in + // AcquireFirstMatchingEventInQueue() above. + ReleaseEvent(currentEvent); + eventProcessed = true; + } + } while (mRunningEventLoop); + + if (eventProcessed) { + moreEvents = + (AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL, + kEventQueueOptionsNone) != NULL); + } + + mRunningEventLoop = wasRunningEventLoop; + + NS_OBJC_END_TRY_ABORT_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; + + AddScreenWakeLockListener(); + + NS_OBJC_TRY_ABORT([NSApp run]); + + RemoveScreenWakeLockListener(); + + return NS_OK; +} + +NS_IMETHODIMP +nsAppShell::Exit(void) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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_ABORT_BLOCK_NSRESULT; +} + +// 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_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +// 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_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + + +// AppShellDelegate implementation + + +@implementation AppShellDelegate +// initWithAppShell: +// +// Constructs the AppShellDelegate object +- (id)initWithAppShell:(nsAppShell*)aAppShell +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(beginMenuTracking:) + name:@"com.apple.HIToolbox.beginMenuTrackingNotification" + object:nil]; + } + + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// applicationWillTerminate: +// +// Notify the nsAppShell that native event processing should be discontinued. +- (void)applicationWillTerminate:(NSNotification*)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mAppShell->WillTerminate(); + + NS_OBJC_END_TRY_ABORT_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_ABORT_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] & NSDeviceIndependentModifierFlagsMask; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// beginMenuTracking +// +// Roll up our context menu (if any) when some other app (or the OS) opens +// any sort of menu. But make sure we don't do this for notifications we +// send ourselves (whose 'sender' will be @"org.mozilla.gecko.PopupWindow"). +- (void)beginMenuTracking:(NSNotification*)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NSString *sender = [aNotification object]; + if (!sender || ![sender isEqualToString:@"org.mozilla.gecko.PopupWindow"]) { + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (rollupWidget) + rollupListener->Rollup(0, true, nullptr, nullptr); + } + + NS_OBJC_END_TRY_ABORT_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 000000000..e7e7ac872 --- /dev/null +++ b/widget/cocoa/nsBidiKeyboard.h @@ -0,0 +1,24 @@ +/* -*- 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 000000000..e0fc86aeb --- /dev/null +++ b/widget/cocoa/nsBidiKeyboard.mm @@ -0,0 +1,42 @@ +/* -*- 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" + +// 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; +} diff --git a/widget/cocoa/nsChangeObserver.h b/widget/cocoa/nsChangeObserver.h new file mode 100644 index 000000000..1b9a00173 --- /dev/null +++ b/widget/cocoa/nsChangeObserver.h @@ -0,0 +1,44 @@ +/* -*- 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 nsIDocument; +class nsIAtom; + +#define NS_DECL_CHANGEOBSERVER \ +void ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute) override; \ +void ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer) override; \ +void ObserveContentInserted(nsIDocument *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 +// nsIDocumentObserver. +// +// Any class that implements this interface must take care to unregister itself +// on deletion. +class nsChangeObserver +{ +public: + // XXX use dom::Element + virtual void ObserveAttributeChanged(nsIDocument* aDocument, + nsIContent* aContent, + nsIAtom* aAttribute)=0; + + virtual void ObserveContentRemoved(nsIDocument* aDocument, + nsIContent* aChild, + int32_t aIndexInContainer)=0; + + virtual void ObserveContentInserted(nsIDocument* 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 000000000..f6fb44633 --- /dev/null +++ b/widget/cocoa/nsChildView.h @@ -0,0 +1,664 @@ +/* -*- 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/Accessible.h" +#include "mozAccessibleProtocol.h" +#endif + +#include "nsISupports.h" +#include "nsBaseWidget.h" +#include "nsWeakPtr.h" +#include "TextInputHandler.h" +#include "nsCocoaUtils.h" +#include "gfxQuartzSurface.h" +#include "GLContextTypes.h" +#include "mozilla/Mutex.h" +#include "nsRegion.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/UniquePtr.h" + +#include "nsString.h" +#include "nsIDragService.h" +#include "ViewRegion.h" + +#import <Carbon/Carbon.h> +#import <Cocoa/Cocoa.h> +#import <AppKit/NSOpenGL.h> + +class nsChildView; +class nsCocoaWindow; + +namespace { +class GLPresenter; +} // namespace + +namespace mozilla { +class InputData; +class PanGestureInput; +class SwipeTracker; +struct SwipeEventQueue; +class VibrancyManager; +namespace layers { +class GLManager; +class IAPZCTreeManager; +} // namespace layers +namespace widget { +class RectTextureImage; +class WidgetRenderingContext; +} // namespace widget +} // namespace mozilla + +@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; + +@end + +@interface NSView (Undocumented) + +// Draws the title string of a window. +// Present on NSThemeFrame since at least 10.6. +// _drawTitleBar is somewhat complex, and has changed over the years +// since OS X 10.6. But in that time it's never done anything that +// would break when called outside of -[NSView drawRect:] (which we +// sometimes do), or whose output can't be redirected to a +// CGContextRef object (which we also sometimes do). This is likely +// to remain true for the indefinite future. However we should +// check _drawTitleBar in each new major version of OS X. For more +// information see bug 877767. +- (void)_drawTitleBar:(NSRect)aRect; + +// Returns an NSRect that is the bounding box for all an NSView's dirty +// rectangles (ones that need to be redrawn). The full list of dirty +// rectangles can be obtained by calling -[NSView _dirtyRegion] and then +// calling -[NSRegion getRects:count:] on what it returns. Both these +// methods have been present in the same form since at least OS X 10.5. +// Unlike -[NSView getRectsBeingDrawn:count:], these methods can be called +// outside a call to -[NSView drawRect:]. +- (NSRect)_dirtyRect; + +// 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 + +@end + +@interface ChildView : NSView< +#ifdef ACCESSIBILITY + mozAccessible, +#endif + mozView, NSTextInputClient> +{ +@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; + + // rects that were invalidated during a draw, so have pending drawing + NSMutableArray* mPendingDirtyRects; + BOOL mPendingFullDisplay; + BOOL mPendingDisplay; + + // 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; + + // Set to YES when our GL surface has been updated and we need to call + // updateGLContext before we composite. + BOOL mNeedsGLUpdate; + + // 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; + + NSOpenGLContext *mGLContext; + + // Simple 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. + // + // mCumulativeMagnification keeps track of the total amount of + // magnification peformed during a magnify gesture so that we can + // send that value with the final MozMagnifyGesture event. + // + // 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 mCumulativeMagnification; + float mCumulativeRotation; + + BOOL mWaitingForPaint; + +#ifdef __LP64__ + // Support for fluid swipe tracking. + BOOL* mCancelSwipeAnimation; +#endif + + // Whether this uses off-main-thread compositing. + BOOL mUsingOMTCompositor; + + // The mask image that's used when painting into the titlebar using basic + // CGContext painting (i.e. non-accelerated). + CGImageRef mTopLeftCornerMask; +} + +// 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)sendFocusEvent:(mozilla::EventMessage)eventMessage; + +- (void)handleMouseMoved:(NSEvent*)aEvent; + +- (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent + enter:(BOOL)aEnter + exitFrom:(mozilla::WidgetMouseEvent::ExitFrom)aExitFrom; + +- (void)updateGLContext; +- (void)_surfaceNeedsUpdate:(NSNotification*)notification; + +- (bool)preRender:(NSOpenGLContext *)aGLContext; +- (void)postRender:(NSOpenGLContext *)aGLContext; + +- (BOOL)isCoveringTitlebar; + +- (void)viewWillStartLiveResize; +- (void)viewDidEndLiveResize; + +- (NSColor*)vibrancyFillColorForThemeGeometryType:(nsITheme::ThemeGeometryType)aThemeGeometryType; +- (NSColor*)vibrancyFontSmoothingBackgroundColorForThemeGeometryType:(nsITheme::ThemeGeometryType)aThemeGeometryType; + +// Simple gestures support +// +// XXX - The swipeWithEvent, beginGestureWithEvent, magnifyWithEvent, +// rotateWithEvent, and endGestureWithEvent methods are part of a +// PRIVATE interface exported by nsResponder and reverse-engineering +// was necessary to obtain the methods' prototypes. Thus, Apple may +// change the interface in the future without notice. +// +// The prototypes were obtained from the following link: +// http://cocoadex.com/2008/02/nsevent-modifications-swipe-ro.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)handleAsyncScrollEvent:(CGEventRef)cgEvent ofType:(CGEventType)type; + +- (void)setUsingOMTCompositor:(BOOL)aUseOMTC; + +- (NSEvent*)lastKeyDownEvent; +@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 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 : public nsBaseWidget +{ +private: + typedef nsBaseWidget Inherited; + typedef mozilla::layers::IAPZCTreeManager IAPZCTreeManager; + +public: + nsChildView(); + + // nsIWidget interface + virtual MOZ_MUST_USE nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData = nullptr) + override; + + virtual void Destroy() override; + + NS_IMETHOD Show(bool aState) override; + virtual bool IsVisible() const override; + + NS_IMETHOD SetParent(nsIWidget* aNewParent) override; + virtual nsIWidget* GetParent(void) override; + virtual float GetDPI() override; + + NS_IMETHOD Move(double aX, double aY) override; + NS_IMETHOD Resize(double aWidth, double aHeight, bool aRepaint) override; + NS_IMETHOD Resize(double aX, double aY, + double aWidth, double aHeight, bool aRepaint) override; + + NS_IMETHOD Enable(bool aState) override; + virtual bool IsEnabled() const override; + NS_IMETHOD SetFocus(bool aRaise) override; + virtual LayoutDeviceIntRect GetBounds() override; + virtual LayoutDeviceIntRect GetClientBounds() override; + virtual LayoutDeviceIntRect GetScreenBounds() override; + + // 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; + + NS_IMETHOD Invalidate(const LayoutDeviceIntRect &aRect) override; + + virtual void* GetNativeData(uint32_t aDataType) override; + virtual nsresult ConfigureChildren(const nsTArray<Configuration>& aConfigurations) override; + virtual LayoutDeviceIntPoint WidgetToScreenOffset() override; + virtual bool ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) override; + + static bool ConvertStatus(nsEventStatus aStatus) + { return aStatus == nsEventStatus_eConsumeNoDefault; } + NS_IMETHOD DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override; + + virtual bool WidgetTypeSupportsAcceleration() override; + virtual bool ShouldUseOffMainThreadCompositing() override; + + NS_IMETHOD SetCursor(nsCursor aCursor) override; + NS_IMETHOD SetCursor(imgIContainer* aCursor, uint32_t aHotspotX, uint32_t aHotspotY) override; + + NS_IMETHOD SetTitle(const nsAString& title) override; + + NS_IMETHOD GetAttention(int32_t aCycleCount) override; + + virtual bool HasPendingInputEvent() override; + + NS_IMETHOD ActivateNativeMenuItemAt(const nsAString& indexString) override; + NS_IMETHOD ForceUpdateNativeMenuAt(const nsAString& indexString) override; + NS_IMETHOD GetSelectionAsPlaintext(nsAString& aResult) override; + + NS_IMETHOD_(void) SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override; + NS_IMETHOD_(InputContext) GetInputContext() override; + NS_IMETHOD_(TextEventDispatcherListener*) + GetNativeTextEventDispatcherListener() override; + NS_IMETHOD AttachNativeKeyEvent(mozilla::WidgetKeyboardEvent& aEvent) override; + NS_IMETHOD_(bool) ExecuteNativeKeyBinding( + NativeKeyBindingsType aType, + const mozilla::WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) override; + bool ExecuteNativeKeyBindingRemapped( + NativeKeyBindingsType aType, + const mozilla::WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData, + uint32_t aGeckoKeyCode, + uint32_t aCocoaKeyCode); + virtual nsIMEUpdatePreference GetIMEUpdatePreference() override; + + virtual nsTransparencyMode GetTransparencyMode() override; + virtual void SetTransparencyMode(nsTransparencyMode aMode) 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, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) override; + + virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint, + nsIObserver* aObserver) override + { return SynthesizeNativeMouseEvent(aPoint, NSMouseMoved, 0, 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; + + // Mac specific methods + + virtual bool DispatchWindowEvent(mozilla::WidgetGUIEvent& event); + + void WillPaintWindow(); + bool PaintWindow(LayoutDeviceIntRegion aRegion); + bool PaintWindowInContext(CGContextRef aContext, const LayoutDeviceIntRegion& aRegion, + mozilla::gfx::IntSize aSurfaceSize); + +#ifdef ACCESSIBILITY + already_AddRefed<mozilla::a11y::Accessible> GetDocumentAccessible(); +#endif + + virtual void CreateCompositor() override; + virtual void PrepareWindowEffects() override; + virtual void CleanupWindowEffects() override; + virtual bool PreRender(mozilla::widget::WidgetRenderingContext* aContext) override; + virtual void PostRender(mozilla::widget::WidgetRenderingContext* aContext) override; + virtual void DrawWindowOverlay(mozilla::widget::WidgetRenderingContext* aManager, + LayoutDeviceIntRect aRect) override; + + virtual void UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) override; + + virtual void UpdateWindowDraggingRegion(const LayoutDeviceIntRegion& aRegion) override; + LayoutDeviceIntRegion GetNonDraggableRegion() { return mNonDraggableRegion.Region(); } + + virtual void ReportSwipeStarted(uint64_t aInputBlockId, bool aStartSwipe) override; + + 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* GetXULWindowWidget(); + + virtual void ReparentNativeWidget(nsIWidget* aNewParent) override; + + mozilla::widget::TextInputHandler* GetTextInputHandler() + { + return mTextInputHandler; + } + + void ClearVibrantAreas(); + NSColor* VibrancyFillColorForThemeGeometryType(nsITheme::ThemeGeometryType aThemeGeometryType); + NSColor* VibrancyFontSmoothingBackgroundColorForThemeGeometryType(nsITheme::ThemeGeometryType aThemeGeometryType); + + // 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()); + } + + already_AddRefed<mozilla::gfx::DrawTarget> + StartRemoteDrawingInRegion(LayoutDeviceIntRegion& aInvalidRegion, + mozilla::layers::BufferMode* aBufferMode) override; + void EndRemoteDrawing() override; + void CleanupRemoteDrawing() override; + bool InitCompositor(mozilla::layers::Compositor* aCompositor) override; + + IAPZCTreeManager* APZCTM() { return mAPZC ; } + + NS_IMETHOD StartPluginIME(const mozilla::WidgetKeyboardEvent& aKeyboardEvent, + int32_t aPanelX, int32_t aPanelY, + nsString& aCommitted) override; + + virtual void SetPluginFocused(bool& aFocused) override; + + bool IsPluginFocused() { return mPluginFocused; } + + virtual LayoutDeviceIntPoint GetClientOffset() override; + + void DispatchAPZWheelInputEvent(mozilla::InputData& aEvent, bool aCanTriggerSwipe); + + void SwipeFinished(); + +protected: + virtual ~nsChildView(); + + void ReportMoveEvent(); + void ReportSizeEvent(); + + // override to create different kinds of child views. Autoreleases, so + // caller must retain. + virtual NSView* CreateCocoaView(NSRect inFrame); + void TearDownView(); + + virtual already_AddRefed<nsIWidget> + AllocateChildPopupWidget() override + { + static NS_DEFINE_IID(kCPopUpCID, NS_POPUP_CID); + nsCOMPtr<nsIWidget> widget = do_CreateInstance(kCPopUpCID); + return widget.forget(); + } + + void ConfigureAPZCTreeManager() override; + void ConfigureAPZControllerThread() override; + + void DoRemoteComposition(const LayoutDeviceIntRect& aRenderRect); + + // Overlay drawing functions for OpenGL drawing + void DrawWindowOverlay(mozilla::layers::GLManager* aManager, LayoutDeviceIntRect aRect); + void MaybeDrawResizeIndicator(mozilla::layers::GLManager* aManager); + void MaybeDrawRoundedCorners(mozilla::layers::GLManager* aManager, const LayoutDeviceIntRect& aRect); + void MaybeDrawTitlebar(mozilla::layers::GLManager* aManager); + + // Redraw the contents of mTitlebarCGContext on the main thread, as + // determined by mDirtyTitlebarRegion. + void UpdateTitlebarCGContext(); + + LayoutDeviceIntRect RectContainingTitlebarControls(); + void UpdateVibrancy(const nsTArray<ThemeGeometry>& aThemeGeometries); + mozilla::VibrancyManager& EnsureVibrancyManager(); + + nsIWidget* GetWidgetForListenerEvents(); + + struct SwipeInfo { + bool wantsSwipe; + uint32_t allowedDirections; + }; + + SwipeInfo SendMayStartSwipe(const mozilla::PanGestureInput& aSwipeStartEvent); + void TrackScrollEventAsSwipe(const mozilla::PanGestureInput& aSwipeStartEvent, + uint32_t aAllowedDirections); + +protected: + + NSView<mozView>* mView; // my parallel cocoa view (ChildView or NativeScrollbarView), [STRONG] + RefPtr<mozilla::widget::TextInputHandler> mTextInputHandler; + InputContext mInputContext; + + NSView<mozView>* 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 + + // Protects the view from being teared down while a composition is in + // progress on the compositor thread. + mozilla::Mutex mViewTearDownLock; + + mozilla::Mutex mEffectsLock; + + // May be accessed from any thread, protected + // by mEffectsLock. + bool mShowsResizeIndicator; + LayoutDeviceIntRect mResizeIndicatorRect; + bool mHasRoundedBottomCorners; + int mDevPixelCornerRadius; + bool mIsCoveringTitlebar; + bool mIsFullscreen; + bool mIsOpaque; + LayoutDeviceIntRect mTitlebarRect; + + // The area of mTitlebarCGContext that needs to be redrawn during the next + // transaction. Accessed from any thread, protected by mEffectsLock. + LayoutDeviceIntRegion mUpdatedTitlebarRegion; + CGContextRef mTitlebarCGContext; + + // Compositor thread only + mozilla::UniquePtr<mozilla::widget::RectTextureImage> mResizerImage; + mozilla::UniquePtr<mozilla::widget::RectTextureImage> mCornerMaskImage; + mozilla::UniquePtr<mozilla::widget::RectTextureImage> mTitlebarImage; + mozilla::UniquePtr<mozilla::widget::RectTextureImage> mBasicCompositorImage; + + // The area of mTitlebarCGContext that has changed and needs to be + // uploaded to to mTitlebarImage. Main thread only. + nsIntRegion mDirtyTitlebarRegion; + + 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; + bool mDrawing; + bool mIsDispatchPaint; // Is a paint event being dispatched + + bool mPluginFocused; + + // Used in OMTC BasicLayers mode. Presents the BasicCompositor result + // surface to the screen using an OpenGL context. + mozilla::UniquePtr<GLPresenter> mGLPresenter; + + mozilla::UniquePtr<mozilla::VibrancyManager> mVibrancyManager; + RefPtr<mozilla::SwipeTracker> mSwipeTracker; + mozilla::UniquePtr<mozilla::SwipeEventQueue> mSwipeEventQueue; + + // Only used for drawRect-based painting in popups. + RefPtr<mozilla::gfx::DrawTarget> mBackingSurface; + + // This flag is only used when APZ is off. It indicates that the current pan + // gesture was processed as a swipe. Sometimes the swipe animation can finish + // before momentum events of the pan gesture have stopped firing, so this + // flag tells us that we shouldn't allow the remaining events to cause + // scrolling. It is reset to false once a new gesture starts (as indicated by + // a PANGESTURE_(MAY)START event). + bool mCurrentPanGestureBelongsToSwipe; + + static uint32_t sLastInputEventCount; + + void ReleaseTitlebarCGContext(); + + // 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 000000000..92ccd8b6c --- /dev/null +++ b/widget/cocoa/nsChildView.mm @@ -0,0 +1,6580 @@ +/* -*- 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 <unistd.h> +#include <math.h> + +#include "nsChildView.h" +#include "nsCocoaWindow.h" + +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" + +#include "nsArrayUtils.h" +#include "nsObjCExceptions.h" +#include "nsCOMPtr.h" +#include "nsToolkit.h" +#include "nsCRT.h" + +#include "nsFontMetrics.h" +#include "nsIRollupListener.h" +#include "nsViewManager.h" +#include "nsIInterfaceRequestor.h" +#include "nsIFile.h" +#include "nsILocalFileMac.h" +#include "nsGfxCIID.h" +#include "nsIDOMSimpleGestureEvent.h" +#include "nsThemeConstants.h" +#include "nsIWidgetListener.h" +#include "nsIPresShell.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 "ComplexTextInputPanel.h" + +#include "gfxContext.h" +#include "gfxQuartzSurface.h" +#include "gfxUtils.h" +#include "nsRegion.h" +#include "Layers.h" +#include "ClientLayerManager.h" +#include "mozilla/layers/LayerManagerComposite.h" +#include "GfxTexturesReporter.h" +#include "GLTextureImage.h" +#include "GLContextProvider.h" +#include "GLContextCGL.h" +#include "ScopedGLHelpers.h" +#include "HeapCopyOfStackArray.h" +#include "mozilla/layers/IAPZCTreeManager.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/GLManager.h" +#include "mozilla/layers/CompositorOGL.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/BasicCompositor.h" +#include "mozilla/layers/InputAPZContext.h" +#include "mozilla/widget/CompositorWidget.h" +#include "gfxUtils.h" +#include "gfxPrefs.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/BorrowedContext.h" +#include "mozilla/gfx/MacIOSurface.h" +#ifdef ACCESSIBILITY +#include "nsAccessibilityService.h" +#include "mozilla/a11y/Platform.h" +#endif +#ifdef MOZ_CRASHREPORTER +#include "nsExceptionHandler.h" +#endif + +#include "mozilla/Preferences.h" + +#include <dlfcn.h> + +#include <ApplicationServices/ApplicationServices.h> + +#include "GeckoProfiler.h" + +#include "nsIDOMWheelEvent.h" +#include "mozilla/layers/ChromeProcessController.h" +#include "nsLayoutUtils.h" +#include "InputData.h" +#include "RectTextureImage.h" +#include "SwipeTracker.h" +#include "VibrancyManager.h" +#include "nsNativeThemeCocoa.h" +#include "nsIDOMWindowUtils.h" +#include "Units.h" +#include "UnitTransforms.h" +#include "mozilla/UniquePtrExtensions.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 + +PRLogModuleInfo* sCocoaLog = nullptr; + +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 + +static bool gChildViewMethodsSwizzled = false; + +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 uint32_t gNumberOfWidgetsNeedingEventThread = 0; + +@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; + +- (NSMenu*)contextMenu; + +- (BOOL)isRectObscuredBySubview:(NSRect)inRect; + +- (void)processPendingRedraws; + +- (void)drawRect:(NSRect)aRect inContext:(CGContextRef)aContext; +- (LayoutDeviceIntRegion)nativeDirtyRegionWithBoundingRect:(NSRect)aRect; +- (BOOL)isUsingMainThreadOpenGL; +- (BOOL)isUsingOpenGL; +- (void)drawUsingOpenGL; +- (void)drawUsingOpenGLCallback; + +- (BOOL)hasRoundedBottomCorners; +- (CGFloat)cornerRadius; +- (void)clearCorners; + +-(void)setGLOpaque:(BOOL)aOpaque; + +// Overlay drawing functions for traditional CGContext drawing +- (void)drawTitleString; +- (void)drawTitlebarHighlight; +- (void)maskTopCornersInContext:(CGContextRef)aContext; + +#if USE_CLICK_HOLD_CONTEXTMENU + // called on a timer two seconds after a mouse down to see if we should display + // a context menu (click-hold) +- (void)clickHoldCallback:(id)inEvent; +#endif + +#ifdef ACCESSIBILITY +- (id<mozAccessible>)accessible; +#endif + +- (LayoutDeviceIntPoint)convertWindowCoordinates:(NSPoint)aPoint; +- (LayoutDeviceIntPoint)convertWindowCoordinatesRoundDown:(NSPoint)aPoint; +- (IAPZCTreeManager*)apzctm; + +- (BOOL)inactiveWindowAcceptsMouseEvent:(NSEvent*)aEvent; +- (void)updateWindowDraggableState; + +- (bool)shouldConsiderStartingSwipeFromEvent:(NSEvent*)aEvent; + +@end + +@interface EventThreadRunner : NSObject +{ + NSThread* mThread; +} +- (id)init; + ++ (void)start; ++ (void)stop; + +@end + +@interface NSView(NSThemeFrameCornerRadius) +- (float)roundedCornerRadius; +@end + +@interface NSView(DraggableRegion) +- (CGSRegionObj)_regionForOpaqueDescendants:(NSRect)aRect forMove:(BOOL)aForMove; +- (CGSRegionObj)_regionForOpaqueDescendants:(NSRect)aRect forMove:(BOOL)aForMove forUnderTitlebar:(BOOL)aForUnderTitlebar; +@end + +@interface NSWindow(NSWindowShouldZoomOnDoubleClick) ++ (BOOL)_shouldZoomOnDoubleClick; // present on 10.7 and above +@end + +// Starting with 10.7 the bottom corners of all windows are rounded. +// Unfortunately, the standard rounding that OS X applies to OpenGL views +// does not use anti-aliasing and looks very crude. Since we want a smooth, +// anti-aliased curve, we'll draw it ourselves. +// Additionally, we need to turn off the OS-supplied rounding because it +// eats into our corner's curve. We do that by overriding an NSSurface method. +@interface NSSurface @end + +@implementation NSSurface(DontCutOffCorners) +- (CGSRegionObj)_createRoundedBottomRegionForRect:(CGRect)rect +{ + // Create a normal rect region without rounded bottom corners. + CGSRegionObj region; + CGSNewRegionWithRect(&rect, ®ion); + return region; +} +@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); +} + +void EnsureLogInitialized() +{ + if (!sCocoaLog) { + sCocoaLog = PR_NewLogModule("nsCocoaWidgets"); + } +} + +namespace { + +// Used for OpenGL drawing from the compositor thread for OMTC BasicLayers. +// We need to use OpenGL for this because there seems to be no other robust +// way of drawing from a secondary thread without locking, which would cause +// deadlocks in our setup. See bug 882523. +class GLPresenter : public GLManager +{ +public: + static mozilla::UniquePtr<GLPresenter> CreateForWindow(nsIWidget* aWindow) + { + // Contrary to CompositorOGL, we allow unaccelerated OpenGL contexts to be + // used. BasicCompositor only requires very basic GL functionality. + RefPtr<GLContext> context = gl::GLContextProvider::CreateForWindow(aWindow, false); + return context ? MakeUnique<GLPresenter>(context) : nullptr; + } + + explicit GLPresenter(GLContext* aContext); + virtual ~GLPresenter(); + + virtual GLContext* gl() const override { return mGLContext; } + virtual ShaderProgramOGL* GetProgram(GLenum aTarget, gfx::SurfaceFormat aFormat) override + { + MOZ_ASSERT(aTarget == LOCAL_GL_TEXTURE_RECTANGLE_ARB); + MOZ_ASSERT(aFormat == gfx::SurfaceFormat::R8G8B8A8); + return mRGBARectProgram.get(); + } + virtual const gfx::Matrix4x4& GetProjMatrix() const override + { + return mProjMatrix; + } + virtual void ActivateProgram(ShaderProgramOGL *aProg) override + { + mGLContext->fUseProgram(aProg->GetProgram()); + } + virtual void BindAndDrawQuad(ShaderProgramOGL *aProg, + const gfx::Rect& aLayerRect, + const gfx::Rect& aTextureRect) override; + + void BeginFrame(LayoutDeviceIntSize aRenderSize); + void EndFrame(); + + NSOpenGLContext* GetNSOpenGLContext() + { + return GLContextCGL::Cast(mGLContext)->GetNSOpenGLContext(); + } + +protected: + RefPtr<mozilla::gl::GLContext> mGLContext; + mozilla::UniquePtr<mozilla::layers::ShaderProgramOGL> mRGBARectProgram; + gfx::Matrix4x4 mProjMatrix; + GLuint mQuadVBO; +}; + +} // unnamed namespace + +namespace mozilla { + +struct SwipeEventQueue { + SwipeEventQueue(uint32_t aAllowedDirections, uint64_t aInputBlockId) + : allowedDirections(aAllowedDirections) + , inputBlockId(aInputBlockId) + {} + + nsTArray<PanGestureInput> queuedEvents; + uint32_t allowedDirections; + uint64_t inputBlockId; +}; + +} // namespace mozilla + +#pragma mark - + +nsChildView::nsChildView() : nsBaseWidget() +, mView(nullptr) +, mParentView(nullptr) +, mParentWidget(nullptr) +, mViewTearDownLock("ChildViewTearDown") +, mEffectsLock("WidgetEffects") +, mShowsResizeIndicator(false) +, mHasRoundedBottomCorners(false) +, mIsCoveringTitlebar(false) +, mIsFullscreen(false) +, mIsOpaque(false) +, mTitlebarCGContext(nullptr) +, mBackingScaleFactor(0.0) +, mVisible(false) +, mDrawing(false) +, mIsDispatchPaint(false) +{ + EnsureLogInitialized(); +} + +nsChildView::~nsChildView() +{ + ReleaseTitlebarCGContext(); + + if (mSwipeTracker) { + mSwipeTracker->Destroy(); + mSwipeTracker = nullptr; + } + + // 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()"); + + DestroyCompositor(); + + if (mAPZC && gfxPrefs::AsyncPanZoomSeparateEventThread()) { + gNumberOfWidgetsNeedingEventThread--; + if (gNumberOfWidgetsNeedingEventThread == 0) { + [EventThreadRunner stop]; + } + } + + // 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. +} + +void +nsChildView::ReleaseTitlebarCGContext() +{ + if (mTitlebarCGContext) { + CGContextRelease(mTitlebarCGContext); + mTitlebarCGContext = nullptr; + } +} + +nsresult +nsChildView::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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; + + // See NSView (MethodSwizzling) below. + if (!gChildViewMethodsSwizzled) { + nsToolkit::SwizzleMethods([NSView class], @selector(mouseDownCanMoveWindow), + @selector(nsChildView_NSView_mouseDownCanMoveWindow)); +#ifdef __LP64__ + nsToolkit::SwizzleMethods([NSEvent class], @selector(addLocalMonitorForEventsMatchingMask:handler:), + @selector(nsChildView_NSEvent_addLocalMonitorForEventsMatchingMask:handler:), + true); + nsToolkit::SwizzleMethods([NSEvent class], @selector(removeMonitor:), + @selector(nsChildView_NSEvent_removeMonitor:), true); +#endif + gChildViewMethodsSwizzled = true; + } + + mBounds = aRect; + + // Ensure that the toolkit is created. + nsToolkit::GetToolkit(); + + BaseCreate(aParent, aInitData); + + // inherit things from the parent view and create our parallel + // NSView in the Cocoa display system + mParentView = nil; + if (aParent) { + // inherit the top-level window. NS_NATIVE_WIDGET is always a NSView + // regardless of if we're asking a window or a view (for compatibility + // with windows). + mParentView = (NSView<mozView>*)aParent->GetNativeData(NS_NATIVE_WIDGET); + mParentWidget = aParent; + } else { + // This is the normal case. When we're the root widget of the view hiararchy, + // aNativeParent will be the contentView of our window, since that's what + // nsCocoaWindow returns when asked for an NS_NATIVE_VIEW. + mParentView = reinterpret_cast<NSView<mozView>*>(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 = [(NSView<mozView>*)CreateCocoaView(r) retain]; + if (!mView) { + return NS_ERROR_FAILURE; + } + + // 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); + + mPluginFocused = false; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Creates the appropriate child view. Override to create something other than +// our |ChildView| object. Autoreleases, so caller must retain. +NSView* +nsChildView::CreateCocoaView(NSRect inFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [[[ChildView alloc] initWithFrame:inFrame geckoChild:this] autorelease]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +void nsChildView::TearDownView() +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +nsCocoaWindow* +nsChildView::GetXULWindowWidget() +{ + id windowDelegate = [[mView window] delegate]; + if (windowDelegate && [windowDelegate isKindOfClass:[WindowDelegate class]]) { + return [(WindowDelegate *)windowDelegate geckoWidget]; + } + return nullptr; +} + +void nsChildView::Destroy() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Make sure that no composition is in progress while disconnecting + // ourselves from the view. + MutexAutoLock lock(mViewTearDownLock); + + 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_ABORT_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_ABORT_BLOCK_NSNULL; + + 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; + } + + return retVal; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; +} + +#pragma mark - + +nsTransparencyMode nsChildView::GetTransparencyMode() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + nsCocoaWindow* windowWidget = GetXULWindowWidget(); + return windowWidget ? windowWidget->GetTransparencyMode() : eTransparencyOpaque; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(eTransparencyOpaque); +} + +// This is called by nsContainerFrame on the root widget for all window types +// except popup windows (when nsCocoaWindow::SetTransparencyMode is used instead). +void nsChildView::SetTransparencyMode(nsTransparencyMode aMode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + nsCocoaWindow* windowWidget = GetXULWindowWidget(); + if (windowWidget) { + windowWidget->SetTransparencyMode(aMode); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +bool nsChildView::IsVisible() const +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (!mVisible) { + return mVisible; + } + + // 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_ABORT_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 +NS_IMETHODIMP nsChildView::Show(bool aState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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; + } + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Change the parent of this widget +NS_IMETHODIMP +nsChildView::SetParent(nsIWidget* aNewParent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (mOnDestroyCalled) + return NS_OK; + + 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); + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void +nsChildView::ReparentNativeWidget(nsIWidget* aNewParent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NS_PRECONDITION(aNewParent, ""); + + 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_ABORT_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() +{ + NSWindow* window = [mView window]; + if (window && [window isKindOfClass:[BaseWindow class]]) { + return [(BaseWindow*)window getDPI]; + } + + return 96.0; +} + +NS_IMETHODIMP nsChildView::Enable(bool aState) +{ + return NS_OK; +} + +bool nsChildView::IsEnabled() const +{ + return true; +} + +NS_IMETHODIMP nsChildView::SetFocus(bool aRaise) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSWindow* window = [mView window]; + if (window) + [window makeFirstResponder:mView]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Override to set the cursor on the mac +NS_IMETHODIMP nsChildView::SetCursor(nsCursor aCursor) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if ([mView isDragInProgress]) + return NS_OK; // Don't change the cursor during dragging. + + nsBaseWidget::SetCursor(aCursor); + return [[nsCursorManager sharedInstance] setCursor:aCursor]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// implement to fix "hidden virtual function" warning +NS_IMETHODIMP nsChildView::SetCursor(imgIContainer* aCursor, + uint32_t aHotspotX, uint32_t aHotspotY) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsBaseWidget::SetCursor(aCursor, aHotspotX, aHotspotY); + return [[nsCursorManager sharedInstance] setCursorWithImage:aCursor hotSpotX:aHotspotX hotSpotY:aHotspotY scaleFactor:BackingScaleFactor()]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +#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; + } + + mBackingScaleFactor = newScale; + NSRect frame = [mView frame]; + mBounds = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, newScale); + + if (mWidgetListener && !mWidgetListener->GetXULWindow()) { + nsIPresShell* presShell = mWidgetListener->GetPresShell(); + if (presShell) { + 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 +NS_IMETHODIMP nsChildView::Move(double aX, double aY) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + int32_t x = NSToIntRound(aX); + int32_t y = NSToIntRound(aY); + + if (!mView || (mBounds.x == x && mBounds.y == y)) + return NS_OK; + + mBounds.x = x; + mBounds.y = y; + + ManipulateViewWithoutNeedingDisplay(mView, ^{ + [mView setFrame:DevPixelsToCocoaPoints(mBounds)]; + }); + + NotifyRollupGeometryChange(); + ReportMoveEvent(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsChildView::Resize(double aWidth, double aHeight, bool aRepaint) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + int32_t width = NSToIntRound(aWidth); + int32_t height = NSToIntRound(aHeight); + + if (!mView || (mBounds.width == width && mBounds.height == height)) + return NS_OK; + + mBounds.width = width; + mBounds.height = height; + + ManipulateViewWithoutNeedingDisplay(mView, ^{ + [mView setFrame:DevPixelsToCocoaPoints(mBounds)]; + }); + + if (mVisible && aRepaint) + [mView setNeedsDisplay:YES]; + + NotifyRollupGeometryChange(); + ReportSizeEvent(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsChildView::Resize(double aX, double aY, + double aWidth, double aHeight, bool aRepaint) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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 NS_OK; + + if (isMoving) { + mBounds.x = x; + mBounds.y = y; + } + if (isResizing) { + mBounds.width = width; + mBounds.height = height; + } + + ManipulateViewWithoutNeedingDisplay(mView, ^{ + [mView setFrame:DevPixelsToCocoaPoints(mBounds)]; + }); + + if (mVisible && aRepaint) + [mView setNeedsDisplay:YES]; + + NotifyRollupGeometryChange(); + if (isMoving) { + ReportMoveEvent(); + if (mOnDestroyCalled) + return NS_OK; + } + if (isResizing) + ReportSizeEvent(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +static const int32_t resizeIndicatorWidth = 15; +static const int32_t resizeIndicatorHeight = 15; +bool nsChildView::ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) +{ + NSView *topLevelView = mView, *superView = nil; + while ((superView = [topLevelView superview])) + topLevelView = superView; + + if (![[topLevelView window] showsResizeIndicator] || + !([[topLevelView window] styleMask] & NSResizableWindowMask)) + return false; + + if (aResizerRect) { + NSSize bounds = [topLevelView bounds].size; + NSPoint corner = NSMakePoint(bounds.width, [topLevelView isFlipped] ? bounds.height : 0); + corner = [topLevelView convertPoint:corner toView:mView]; + aResizerRect->SetRect(NSToIntRound(corner.x) - resizeIndicatorWidth, + NSToIntRound(corner.y) - resizeIndicatorHeight, + resizeIndicatorWidth, resizeIndicatorHeight); + } + return true; +} + +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, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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); + + NSEvent* event = [NSEvent mouseEventWithType:(NSEventType)aNativeMessage + location:windowPoint + modifierFlags:aModifierFlags + 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 (aNativeMessage == NSMouseEntered) { + [window mouseEntered:event]; + return NS_OK; + } + if (aNativeMessage == NSMouseExited) { + [window mouseExited:event]; + return NS_OK; + } + if (aNativeMessage == NSMouseMoved) { + [window mouseMoved:event]; + return NS_OK; + } + } + + [NSApp sendEvent:event]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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_ABORT_BLOCK_NSRESULT; + + 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, aDeltaY, aDeltaX, aDeltaZ); + if (!cgEvent) { + return NS_ERROR_FAILURE; + } + + CGEventPost(kCGHIDEventTap, cgEvent); + CFRelease(cgEvent); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsChildView::SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + mozilla::LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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(), PR_IntervalNow(), TimeStamp::Now(), + aPointerId, aPointerState, pointInWindow, aPointerPressure, + aPointerOrientation); + DispatchTouchInput(inputToDispatch); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// First argument has to be an NSMenu representing the application's top-level +// menu bar. The returned item is *not* retained. +static NSMenuItem* NativeMenuItemWithLocation(NSMenu* menubar, NSString* locationString) +{ + NSArray* indexes = [locationString componentsSeparatedByString:@"|"]; + unsigned int indexCount = [indexes count]; + if (indexCount == 0) + return nil; + + NSMenu* currentSubmenu = [NSApp mainMenu]; + for (unsigned int i = 0; i < indexCount; i++) { + int targetIndex; + // We remove the application menu from consideration for the top-level menu + if (i == 0) + targetIndex = [[indexes objectAtIndex:i] intValue] + 1; + else + targetIndex = [[indexes objectAtIndex:i] intValue]; + int itemCount = [currentSubmenu numberOfItems]; + if (targetIndex < itemCount) { + NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex]; + // if this is the last index just return the menu item + if (i == (indexCount - 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; +} + +// Used for testing native menu system structure and event handling. +NS_IMETHODIMP nsChildView::ActivateNativeMenuItemAt(const nsAString& indexString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSString* locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading()) + length:indexString.Length()]; + NSMenuItem* item = NativeMenuItemWithLocation([NSApp mainMenu], locationString); + // 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]); + [parent performActionForItemAtIndex:[parent indexOfItem:item]]; + return NS_OK; + } + } + return NS_ERROR_FAILURE; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Used for testing native menu system structure and event handling. +NS_IMETHODIMP nsChildView::ForceUpdateNativeMenuAt(const nsAString& indexString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsCocoaWindow *widget = GetXULWindowWidget(); + if (widget) { + nsMenuBarX* mb = widget->GetMenuBar(); + if (mb) { + if (indexString.IsEmpty()) + mb->ForceNativeMenuReload(); + else + mb->ForceUpdateNativeMenuAt(indexString); + } + } + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +#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 +NS_IMETHODIMP nsChildView::Invalidate(const LayoutDeviceIntRect& aRect) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mView || !mVisible) + return NS_OK; + + NS_ASSERTION(GetLayerManager()->GetBackendType() != LayersBackend::LAYERS_CLIENT, + "Shouldn't need to invalidate with accelerated OMTC layers!"); + + if ([NSView focusView]) { + // if a view is focussed (i.e. being drawn), then postpone the invalidate so that we + // don't lose it. + [mView setNeedsPendingDisplayInRect:DevPixelsToCocoaPoints(aRect)]; + } + else { + [mView setNeedsDisplayInRect:DevPixelsToCocoaPoints(aRect)]; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +bool +nsChildView::WidgetTypeSupportsAcceleration() +{ + // Don't use OpenGL for transparent windows or for popup windows. + return mView && [[mView window] isOpaque] && + ![[mView window] isKindOfClass:[PopupWindow class]]; +} + +bool +nsChildView::ShouldUseOffMainThreadCompositing() +{ + // Don't use OMTC for transparent windows or for popup windows. + if (!mView || ![[mView window] isOpaque] || + [[mView window] isKindOfClass:[PopupWindow class]]) + return false; + + return nsBaseWidget::ShouldUseOffMainThreadCompositing(); +} + +inline uint16_t COLOR8TOCOLOR16(uint8_t color8) +{ + // return (color8 == 0xFF ? 0xFFFF : (color8 << 8)); + return (color8 << 8) | color8; /* (color8 * 257) == (color8 * 0x0101) */ +} + +#pragma mark - + +nsresult nsChildView::ConfigureChildren(const nsTArray<Configuration>& aConfigurations) +{ + return NS_OK; +} + +// Invokes callback and ProcessEvent methods on Event Listener object +NS_IMETHODIMP nsChildView::DispatchEvent(WidgetGUIEvent* event, + nsEventStatus& aStatus) +{ + RefPtr<nsChildView> kungFuDeathGrip(this); + +#ifdef DEBUG + debug_DumpEvent(stdout, event->mWidget, event, "something", 0); +#endif + + NS_ASSERTION(!(mTextInputHandler && mTextInputHandler->IsIMEComposing() && + event->HasKeyEventMessage()), + "Any key events should not be fired during IME composing"); + + 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; +} + +bool nsChildView::DispatchWindowEvent(WidgetGUIEvent& event) +{ + nsEventStatus status; + DispatchEvent(&event, status); + return ConvertStatus(status); +} + +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::PaintWindowInContext(CGContextRef aContext, const LayoutDeviceIntRegion& aRegion, gfx::IntSize aSurfaceSize) +{ + if (!mBackingSurface || mBackingSurface->GetSize() != aSurfaceSize) { + mBackingSurface = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(aSurfaceSize, + gfx::SurfaceFormat::B8G8R8A8); + if (!mBackingSurface) { + return false; + } + } + + RefPtr<gfxContext> targetContext = gfxContext::CreateOrNull(mBackingSurface); + MOZ_ASSERT(targetContext); // already checked the draw target above + + // 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)); + mBackingSurface->ClearRect(gfx::Rect(r.ToUnknownRect())); + } + targetContext->Clip(); + + nsAutoRetainCocoaObject kungFuDeathGrip(mView); + bool painted = false; + if (GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC) { + nsBaseWidget::AutoLayerManagerSetup + setupLayerManager(this, targetContext, BufferMode::BUFFER_NONE); + painted = PaintWindow(aRegion); + } else if (GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT) { + // We only need this so that we actually get DidPaintWindow fired + painted = PaintWindow(aRegion); + } + + uint8_t* data; + gfx::IntSize size; + int32_t stride; + gfx::SurfaceFormat format; + + if (!mBackingSurface->LockBits(&data, &size, &stride, &format)) { + return false; + } + + // Draw the backing surface onto the window. + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, data, stride * size.height, NULL); + NSColorSpace* colorSpace = [[mView window] colorSpace]; + CGImageRef image = CGImageCreate(size.width, size.height, 8, 32, stride, + [colorSpace CGColorSpace], + kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst, + provider, NULL, false, kCGRenderingIntentDefault); + CGContextSaveGState(aContext); + CGContextTranslateCTM(aContext, 0, size.height); + CGContextScaleCTM(aContext, 1, -1); + CGContextSetBlendMode(aContext, kCGBlendModeCopy); + CGContextDrawImage(aContext, CGRectMake(0, 0, size.width, size.height), image); + CGImageRelease(image); + CGDataProviderRelease(provider); + CGContextRestoreGState(aContext); + + mBackingSurface->ReleaseBits(data); + + return painted; +} + +#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_ABORT_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_ABORT_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_ABORT_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_ABORT_BLOCK_RETURN(LayoutDeviceIntPoint(0,0)); +} + +NS_IMETHODIMP nsChildView::SetTitle(const nsAString& title) +{ + // child views don't have titles + return NS_OK; +} + +NS_IMETHODIMP nsChildView::GetAttention(int32_t aCycleCount) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + [NSApp requestUserAttention:NSInformationalRequest]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +/* 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 - + +NS_IMETHODIMP +nsChildView::StartPluginIME(const mozilla::WidgetKeyboardEvent& aKeyboardEvent, + int32_t aPanelX, int32_t aPanelY, + nsString& aCommitted) +{ + NS_ENSURE_TRUE(mView, NS_ERROR_NOT_AVAILABLE); + + ComplexTextInputPanel* ctiPanel = + ComplexTextInputPanel::GetSharedComplexTextInputPanel(); + + ctiPanel->PlacePanel(aPanelX, aPanelY); + // We deliberately don't use TextInputHandler::GetCurrentKeyEvent() to + // obtain the NSEvent* we pass to InterpretKeyEvent(). This works fine in + // non-e10s mode. But in e10s mode TextInputHandler::HandleKeyDownEvent() + // has already returned, so the relevant KeyEventState* (and its NSEvent*) + // is already out of scope. Furthermore we don't *need* to use it. + // StartPluginIME() is only ever called to start a new IME session when none + // currently exists. So nested IME should never reach here, and so it should + // be fine to use the last key-down event received by -[ChildView keyDown:] + // (as we currently do). + ctiPanel->InterpretKeyEvent([(ChildView*)mView lastKeyDownEvent], aCommitted); + + return NS_OK; +} + +void +nsChildView::SetPluginFocused(bool& aFocused) +{ + if (aFocused == mPluginFocused) { + return; + } + if (!aFocused) { + ComplexTextInputPanel* ctiPanel = + ComplexTextInputPanel::GetSharedComplexTextInputPanel(); + if (ctiPanel) { + ctiPanel->CancelComposition(); + } + } + mPluginFocused = aFocused; +} + +NS_IMETHODIMP_(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(); + } + } + + mInputContext = aContext; + switch (aContext.mIMEState.mEnabled) { + case IMEState::ENABLED: + case IMEState::PLUGIN: + mTextInputHandler->SetASCIICapableOnly(false); + mTextInputHandler->EnableIME(true); + if (mInputContext.mIMEState.mOpen != IMEState::DONT_CHANGE_OPEN_STATE) { + mTextInputHandler->SetIMEOpenState( + mInputContext.mIMEState.mOpen == IMEState::OPEN); + } + break; + case IMEState::DISABLED: + mTextInputHandler->SetASCIICapableOnly(false); + mTextInputHandler->EnableIME(false); + break; + case IMEState::PASSWORD: + mTextInputHandler->SetASCIICapableOnly(true); + mTextInputHandler->EnableIME(false); + break; + default: + NS_ERROR("not implemented!"); + } +} + +NS_IMETHODIMP_(InputContext) +nsChildView::GetInputContext() +{ + switch (mInputContext.mIMEState.mEnabled) { + case IMEState::ENABLED: + case IMEState::PLUGIN: + if (mTextInputHandler) { + mInputContext.mIMEState.mOpen = + mTextInputHandler->IsIMEOpened() ? IMEState::OPEN : IMEState::CLOSED; + break; + } + // If mTextInputHandler is null, set CLOSED instead... + MOZ_FALLTHROUGH; + default: + mInputContext.mIMEState.mOpen = IMEState::CLOSED; + break; + } + return mInputContext; +} + +NS_IMETHODIMP_(TextEventDispatcherListener*) +nsChildView::GetNativeTextEventDispatcherListener() +{ + if (NS_WARN_IF(!mTextInputHandler)) { + return nullptr; + } + return mTextInputHandler; +} + +NS_IMETHODIMP +nsChildView::AttachNativeKeyEvent(mozilla::WidgetKeyboardEvent& aEvent) +{ + NS_ENSURE_TRUE(mTextInputHandler, NS_ERROR_NOT_AVAILABLE); + return mTextInputHandler->AttachNativeKeyEvent(aEvent); +} + +bool +nsChildView::ExecuteNativeKeyBindingRemapped(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData, + uint32_t aGeckoKeyCode, + uint32_t aCocoaKeyCode) +{ + NSEvent *originalEvent = reinterpret_cast<NSEvent*>(aEvent.mNativeKeyEvent); + + WidgetKeyboardEvent modifiedEvent(aEvent); + modifiedEvent.mKeyCode = aGeckoKeyCode; + + unichar ch = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aGeckoKeyCode); + NSString *chars = + [[[NSString alloc] initWithCharacters:&ch length:1] autorelease]; + + modifiedEvent.mNativeKeyEvent = + [NSEvent keyEventWithType:[originalEvent type] + location:[originalEvent locationInWindow] + modifierFlags:[originalEvent modifierFlags] + timestamp:[originalEvent timestamp] + windowNumber:[originalEvent windowNumber] + context:[originalEvent context] + characters:chars + charactersIgnoringModifiers:chars + isARepeat:[originalEvent isARepeat] + keyCode:aCocoaKeyCode]; + + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + return keyBindings->Execute(modifiedEvent, aCallback, aCallbackData); +} + +NS_IMETHODIMP_(bool) +nsChildView::ExecuteNativeKeyBinding(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) +{ + // If the key is a cursor-movement arrow, and the current selection has + // vertical writing-mode, we'll remap so that the movement command + // generated (in terms of characters/lines) will be appropriate for + // the physical direction of the arrow. + if (aEvent.mKeyCode >= NS_VK_LEFT && aEvent.mKeyCode <= NS_VK_DOWN) { + WidgetQueryContentEvent query(true, eQuerySelectedText, this); + DispatchWindowEvent(query); + + if (query.mSucceeded && query.mReply.mWritingMode.IsVertical()) { + uint32_t geckoKey = 0; + uint32_t cocoaKey = 0; + + switch (aEvent.mKeyCode) { + case NS_VK_LEFT: + if (query.mReply.mWritingMode.IsVerticalLR()) { + geckoKey = NS_VK_UP; + cocoaKey = kVK_UpArrow; + } else { + geckoKey = NS_VK_DOWN; + cocoaKey = kVK_DownArrow; + } + break; + + case NS_VK_RIGHT: + if (query.mReply.mWritingMode.IsVerticalLR()) { + geckoKey = NS_VK_DOWN; + cocoaKey = kVK_DownArrow; + } else { + geckoKey = NS_VK_UP; + cocoaKey = kVK_UpArrow; + } + break; + + case NS_VK_UP: + geckoKey = NS_VK_LEFT; + cocoaKey = kVK_LeftArrow; + break; + + case NS_VK_DOWN: + geckoKey = NS_VK_RIGHT; + cocoaKey = kVK_RightArrow; + break; + } + + return ExecuteNativeKeyBindingRemapped(aType, aEvent, aCallback, + aCallbackData, + geckoKey, cocoaKey); + } + } + + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + return keyBindings->Execute(aEvent, aCallback, aCallbackData); +} + +nsIMEUpdatePreference +nsChildView::GetIMEUpdatePreference() +{ + // 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 nsIMEUpdatePreference(); +} + +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 textContent(true, eQueryTextContent, this); + textContent.InitForQueryTextContent(0, 0); + DispatchWindowEvent(textContent); + if (textContent.mSucceeded && textContent.mReply.mFocusedWidget) { + NSView<mozView>* view = static_cast<NSView<mozView>*>( + textContent.mReply.mFocusedWidget->GetNativeData(NS_NATIVE_WIDGET)); + if (view) + editorView = view; + } + return editorView; +} + +#pragma mark - + +void +nsChildView::CreateCompositor() +{ + nsBaseWidget::CreateCompositor(); + if (mCompositorBridgeChild) { + [(ChildView *)mView setUsingOMTCompositor:true]; + } +} + +void +nsChildView::ConfigureAPZCTreeManager() +{ + nsBaseWidget::ConfigureAPZCTreeManager(); + + if (gfxPrefs::AsyncPanZoomSeparateEventThread()) { + if (gNumberOfWidgetsNeedingEventThread == 0) { + [EventThreadRunner start]; + } + gNumberOfWidgetsNeedingEventThread++; + } +} + +void +nsChildView::ConfigureAPZControllerThread() +{ + if (gfxPrefs::AsyncPanZoomSeparateEventThread()) { + // The EventThreadRunner is the controller thread, but it doesn't + // have a MessageLoop. + APZThreadUtils::SetControllerThread(nullptr); + } else { + nsBaseWidget::ConfigureAPZControllerThread(); + } +} + +LayoutDeviceIntRect +nsChildView::RectContainingTitlebarControls() +{ + // Start with a thin strip at the top of the window for the highlight line. + NSRect rect = NSMakeRect(0, 0, [mView bounds].size.width, + [(ChildView*)mView cornerRadius]); + + // If we draw the titlebar title string, increase the height to the default + // titlebar height. This height does not necessarily include all the titlebar + // controls because we may have moved them further down, but at least it will + // include the whole title text. + BaseWindow* window = (BaseWindow*)[mView window]; + if ([window wantsTitleDrawn] && [window isKindOfClass:[ToolbarWindow class]]) { + CGFloat defaultTitlebarHeight = [(ToolbarWindow*)window titlebarHeight]; + rect.size.height = std::max(rect.size.height, defaultTitlebarHeight); + } + + // Add the rects of the titlebar controls. + for (id view in [window titlebarControls]) { + rect = NSUnionRect(rect, [mView convertRect:[view bounds] fromView:view]); + } + return CocoaPointsToDevPixels(rect); +} + +void +nsChildView::PrepareWindowEffects() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + bool canBeOpaque; + { + MutexAutoLock lock(mEffectsLock); + mShowsResizeIndicator = ShowsResizeIndicator(&mResizeIndicatorRect); + mHasRoundedBottomCorners = [(ChildView*)mView hasRoundedBottomCorners]; + CGFloat cornerRadius = [(ChildView*)mView cornerRadius]; + mDevPixelCornerRadius = cornerRadius * BackingScaleFactor(); + mIsCoveringTitlebar = [(ChildView*)mView isCoveringTitlebar]; + NSInteger styleMask = [[mView window] styleMask]; + bool wasFullscreen = mIsFullscreen; + mIsFullscreen = (styleMask & NSFullScreenWindowMask) || !(styleMask & NSTitledWindowMask); + + canBeOpaque = mIsFullscreen && wasFullscreen; + if (canBeOpaque && VibrancyManager::SystemSupportsVibrancy()) { + canBeOpaque = !EnsureVibrancyManager().HasVibrantRegions(); + } + if (mIsCoveringTitlebar) { + mTitlebarRect = RectContainingTitlebarControls(); + UpdateTitlebarCGContext(); + } + } + + // If we've just transitioned into or out of full screen then update the opacity on our GLContext. + if (canBeOpaque != mIsOpaque) { + mIsOpaque = canBeOpaque; + [(ChildView*)mView setGLOpaque:canBeOpaque]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsChildView::CleanupWindowEffects() +{ + mResizerImage = nullptr; + mCornerMaskImage = nullptr; + mTitlebarImage = nullptr; +} + +bool +nsChildView::PreRender(WidgetRenderingContext* aContext) +{ + UniquePtr<GLManager> manager(GLManager::CreateGLManager(aContext->mLayerManager)); + if (!manager) { + return true; + } + + // 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. + mViewTearDownLock.Lock(); + + NSOpenGLContext *glContext = GLContextCGL::Cast(manager->gl())->GetNSOpenGLContext(); + + if (![(ChildView*)mView preRender:glContext]) { + mViewTearDownLock.Unlock(); + return false; + } + return true; +} + +void +nsChildView::PostRender(WidgetRenderingContext* aContext) +{ + UniquePtr<GLManager> manager(GLManager::CreateGLManager(aContext->mLayerManager)); + if (!manager) { + return; + } + NSOpenGLContext *glContext = GLContextCGL::Cast(manager->gl())->GetNSOpenGLContext(); + [(ChildView*)mView postRender:glContext]; + mViewTearDownLock.Unlock(); +} + +void +nsChildView::DrawWindowOverlay(WidgetRenderingContext* aContext, + LayoutDeviceIntRect aRect) +{ + mozilla::UniquePtr<GLManager> manager(GLManager::CreateGLManager(aContext->mLayerManager)); + if (manager) { + DrawWindowOverlay(manager.get(), aRect); + } +} + +void +nsChildView::DrawWindowOverlay(GLManager* aManager, LayoutDeviceIntRect aRect) +{ + GLContext* gl = aManager->gl(); + ScopedGLState scopedScissorTestState(gl, LOCAL_GL_SCISSOR_TEST, false); + + MaybeDrawTitlebar(aManager); + MaybeDrawResizeIndicator(aManager); + MaybeDrawRoundedCorners(aManager, aRect); +} + +static void +ClearRegion(gfx::DrawTarget *aDT, LayoutDeviceIntRegion aRegion) +{ + gfxUtils::ClipToRegion(aDT, aRegion.ToUnknownRegion()); + aDT->ClearRect(gfx::Rect(0, 0, aDT->GetSize().width, aDT->GetSize().height)); + aDT->PopClip(); +} + +static void +DrawResizer(CGContextRef aCtx) +{ + CGContextSetShouldAntialias(aCtx, false); + CGPoint points[6]; + points[0] = CGPointMake(13.0f, 4.0f); + points[1] = CGPointMake(3.0f, 14.0f); + points[2] = CGPointMake(13.0f, 8.0f); + points[3] = CGPointMake(7.0f, 14.0f); + points[4] = CGPointMake(13.0f, 12.0f); + points[5] = CGPointMake(11.0f, 14.0f); + CGContextSetRGBStrokeColor(aCtx, 0.00f, 0.00f, 0.00f, 0.15f); + CGContextStrokeLineSegments(aCtx, points, 6); + + points[0] = CGPointMake(13.0f, 5.0f); + points[1] = CGPointMake(4.0f, 14.0f); + points[2] = CGPointMake(13.0f, 9.0f); + points[3] = CGPointMake(8.0f, 14.0f); + points[4] = CGPointMake(13.0f, 13.0f); + points[5] = CGPointMake(12.0f, 14.0f); + CGContextSetRGBStrokeColor(aCtx, 0.13f, 0.13f, 0.13f, 0.54f); + CGContextStrokeLineSegments(aCtx, points, 6); + + points[0] = CGPointMake(13.0f, 6.0f); + points[1] = CGPointMake(5.0f, 14.0f); + points[2] = CGPointMake(13.0f, 10.0f); + points[3] = CGPointMake(9.0f, 14.0f); + points[5] = CGPointMake(13.0f, 13.9f); + points[4] = CGPointMake(13.0f, 14.0f); + CGContextSetRGBStrokeColor(aCtx, 0.84f, 0.84f, 0.84f, 0.55f); + CGContextStrokeLineSegments(aCtx, points, 6); +} + +void +nsChildView::MaybeDrawResizeIndicator(GLManager* aManager) +{ + MutexAutoLock lock(mEffectsLock); + if (!mShowsResizeIndicator) { + return; + } + + if (!mResizerImage) { + mResizerImage = MakeUnique<RectTextureImage>(); + } + + LayoutDeviceIntSize size = mResizeIndicatorRect.Size(); + mResizerImage->UpdateIfNeeded(size, LayoutDeviceIntRegion(), ^(gfx::DrawTarget* drawTarget, const LayoutDeviceIntRegion& updateRegion) { + ClearRegion(drawTarget, updateRegion); + gfx::BorrowedCGContext borrow(drawTarget); + DrawResizer(borrow.cg); + borrow.Finish(); + }); + + mResizerImage->Draw(aManager, mResizeIndicatorRect.TopLeft()); +} + +// Draw the highlight line at the top of the titlebar. +// This function draws into the current NSGraphicsContext and assumes flippedness. +static void +DrawTitlebarHighlight(NSSize aWindowSize, CGFloat aRadius, CGFloat aDevicePixelWidth) +{ + [NSGraphicsContext saveGraphicsState]; + + // Set up the clip path. We start with the outer rectangle and cut out a + // slightly smaller inner rectangle with rounded corners. + // The outer corners of the resulting path will be square, but they will be + // masked away in a later step. + NSBezierPath* path = [NSBezierPath bezierPath]; + [path setWindingRule:NSEvenOddWindingRule]; + NSRect pathRect = NSMakeRect(0, 0, aWindowSize.width, aRadius + 2); + [path appendBezierPathWithRect:pathRect]; + pathRect = NSInsetRect(pathRect, aDevicePixelWidth, aDevicePixelWidth); + CGFloat innerRadius = aRadius - aDevicePixelWidth; + [path appendBezierPathWithRoundedRect:pathRect xRadius:innerRadius yRadius:innerRadius]; + [path addClip]; + + // Now we fill the path with a subtle highlight gradient. + // We don't use NSGradient because it's 5x to 15x slower than the manual fill, + // as indicated by the performance test in bug 880620. + for (CGFloat y = 0; y < aRadius; y += aDevicePixelWidth) { + CGFloat t = y / aRadius; + [[NSColor colorWithDeviceWhite:1.0 alpha:0.4 * (1.0 - t)] set]; + NSRectFillUsingOperation(NSMakeRect(0, y, aWindowSize.width, aDevicePixelWidth), NSCompositeSourceOver); + } + + [NSGraphicsContext restoreGraphicsState]; +} + +static CGContextRef +CreateCGContext(const LayoutDeviceIntSize& aSize) +{ + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = + CGBitmapContextCreate(NULL, + aSize.width, + aSize.height, + 8 /* bitsPerComponent */, + aSize.width * 4, + cs, + kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(cs); + + CGContextTranslateCTM(ctx, 0, aSize.height); + CGContextScaleCTM(ctx, 1, -1); + CGContextSetInterpolationQuality(ctx, kCGInterpolationLow); + + return ctx; +} + +LayoutDeviceIntSize +TextureSizeForSize(const LayoutDeviceIntSize& aSize) +{ + return LayoutDeviceIntSize(RoundUpPow2(aSize.width), + RoundUpPow2(aSize.height)); +} + +// When this method is entered, mEffectsLock is already being held. +void +nsChildView::UpdateTitlebarCGContext() +{ + if (mTitlebarRect.IsEmpty()) { + ReleaseTitlebarCGContext(); + return; + } + + NSRect titlebarRect = DevPixelsToCocoaPoints(mTitlebarRect); + NSRect dirtyRect = [mView convertRect:[(BaseWindow*)[mView window] getAndResetNativeDirtyRect] fromView:nil]; + NSRect dirtyTitlebarRect = NSIntersectionRect(titlebarRect, dirtyRect); + + LayoutDeviceIntSize texSize = TextureSizeForSize(mTitlebarRect.Size()); + if (!mTitlebarCGContext || + CGBitmapContextGetWidth(mTitlebarCGContext) != size_t(texSize.width) || + CGBitmapContextGetHeight(mTitlebarCGContext) != size_t(texSize.height)) { + dirtyTitlebarRect = titlebarRect; + + ReleaseTitlebarCGContext(); + + mTitlebarCGContext = CreateCGContext(texSize); + } + + if (NSIsEmptyRect(dirtyTitlebarRect)) { + return; + } + + CGContextRef ctx = mTitlebarCGContext; + + CGContextSaveGState(ctx); + + double scale = BackingScaleFactor(); + CGContextScaleCTM(ctx, scale, scale); + + CGContextClipToRect(ctx, NSRectToCGRect(dirtyTitlebarRect)); + CGContextClearRect(ctx, NSRectToCGRect(dirtyTitlebarRect)); + + NSGraphicsContext* oldContext = [NSGraphicsContext currentContext]; + + CGContextSaveGState(ctx); + + BaseWindow* window = (BaseWindow*)[mView window]; + NSView* frameView = [[window contentView] superview]; + if (![frameView isFlipped]) { + CGContextTranslateCTM(ctx, 0, [frameView bounds].size.height); + CGContextScaleCTM(ctx, 1, -1); + } + NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:[frameView isFlipped]]; + [NSGraphicsContext setCurrentContext:context]; + + // Draw the title string. + if ([window wantsTitleDrawn] && [frameView respondsToSelector:@selector(_drawTitleBar:)]) { + [frameView _drawTitleBar:[frameView bounds]]; + } + + // Draw the titlebar controls into the titlebar image. + for (id view in [window titlebarControls]) { + NSRect viewFrame = [view frame]; + NSRect viewRect = [mView convertRect:viewFrame fromView:frameView]; + if (!NSIntersectsRect(dirtyTitlebarRect, viewRect)) { + continue; + } + // All of the titlebar controls we're interested in are subclasses of + // NSButton. + if (![view isKindOfClass:[NSButton class]]) { + continue; + } + NSButton *button = (NSButton *) view; + id cellObject = [button cell]; + if (![cellObject isKindOfClass:[NSCell class]]) { + continue; + } + NSCell *cell = (NSCell *) cellObject; + + CGContextSaveGState(ctx); + CGContextTranslateCTM(ctx, viewFrame.origin.x, viewFrame.origin.y); + + if ([context isFlipped] != [view isFlipped]) { + CGContextTranslateCTM(ctx, 0, viewFrame.size.height); + CGContextScaleCTM(ctx, 1, -1); + } + + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:[view isFlipped]]]; + + if ([window useBrightTitlebarForeground] && !nsCocoaFeatures::OnYosemiteOrLater() && + view == [window standardWindowButton:NSWindowFullScreenButton]) { + // Make the fullscreen button visible on dark titlebar backgrounds by + // drawing it into a new transparency layer and turning it white. + CGRect r = NSRectToCGRect([view bounds]); + CGContextBeginTransparencyLayerWithRect(ctx, r, nullptr); + + // Draw twice for double opacity. + [cell drawWithFrame:[button bounds] inView:button]; + [cell drawWithFrame:[button bounds] inView:button]; + + // Make it white. + CGContextSetBlendMode(ctx, kCGBlendModeSourceIn); + CGContextSetRGBFillColor(ctx, 1, 1, 1, 1); + CGContextFillRect(ctx, r); + CGContextSetBlendMode(ctx, kCGBlendModeNormal); + + CGContextEndTransparencyLayer(ctx); + } else { + [cell drawWithFrame:[button bounds] inView:button]; + } + + [NSGraphicsContext setCurrentContext:context]; + CGContextRestoreGState(ctx); + } + + CGContextRestoreGState(ctx); + + DrawTitlebarHighlight([frameView bounds].size, [(ChildView*)mView cornerRadius], + DevPixelsToCocoaPoints(1)); + + [NSGraphicsContext setCurrentContext:oldContext]; + + CGContextRestoreGState(ctx); + + mUpdatedTitlebarRegion.OrWith(CocoaPointsToDevPixels(dirtyTitlebarRect)); +} + +// This method draws an overlay in the top of the window which contains the +// titlebar controls (e.g. close, min, zoom, fullscreen) and the titlebar +// highlight effect. +// This is necessary because the real titlebar controls are covered by our +// OpenGL context. Note that in terms of the NSView hierarchy, our ChildView +// is actually below the titlebar controls - that's why hovering and clicking +// them works as expected - but their visual representation is only drawn into +// the normal window buffer, and the window buffer surface lies below the +// GLContext surface. In order to make the titlebar controls visible, we have +// to redraw them inside the OpenGL context surface. +void +nsChildView::MaybeDrawTitlebar(GLManager* aManager) +{ + MutexAutoLock lock(mEffectsLock); + if (!mIsCoveringTitlebar || mIsFullscreen) { + return; + } + + LayoutDeviceIntRegion updatedTitlebarRegion; + updatedTitlebarRegion.And(mUpdatedTitlebarRegion, mTitlebarRect); + mUpdatedTitlebarRegion.SetEmpty(); + + if (!mTitlebarImage) { + mTitlebarImage = MakeUnique<RectTextureImage>(); + } + + mTitlebarImage->UpdateFromCGContext(mTitlebarRect.Size(), + updatedTitlebarRegion, + mTitlebarCGContext); + + mTitlebarImage->Draw(aManager, mTitlebarRect.TopLeft()); +} + +static void +DrawTopLeftCornerMask(CGContextRef aCtx, int aRadius) +{ + CGContextSetRGBFillColor(aCtx, 1.0, 1.0, 1.0, 1.0); + CGContextFillEllipseInRect(aCtx, CGRectMake(0, 0, aRadius * 2, aRadius * 2)); +} + +void +nsChildView::MaybeDrawRoundedCorners(GLManager* aManager, + const LayoutDeviceIntRect& aRect) +{ + MutexAutoLock lock(mEffectsLock); + + if (!mCornerMaskImage) { + mCornerMaskImage = MakeUnique<RectTextureImage>(); + } + + LayoutDeviceIntSize size(mDevPixelCornerRadius, mDevPixelCornerRadius); + mCornerMaskImage->UpdateIfNeeded(size, LayoutDeviceIntRegion(), ^(gfx::DrawTarget* drawTarget, const LayoutDeviceIntRegion& updateRegion) { + ClearRegion(drawTarget, updateRegion); + RefPtr<gfx::PathBuilder> builder = drawTarget->CreatePathBuilder(); + builder->Arc(gfx::Point(mDevPixelCornerRadius, mDevPixelCornerRadius), mDevPixelCornerRadius, 0, 2.0f * M_PI); + RefPtr<gfx::Path> path = builder->Finish(); + drawTarget->Fill(path, + gfx::ColorPattern(gfx::Color(1.0, 1.0, 1.0, 1.0)), + gfx::DrawOptions(1.0f, gfx::CompositionOp::OP_SOURCE)); + }); + + // Use operator destination in: multiply all 4 channels with source alpha. + aManager->gl()->fBlendFuncSeparate(LOCAL_GL_ZERO, LOCAL_GL_SRC_ALPHA, + LOCAL_GL_ZERO, LOCAL_GL_SRC_ALPHA); + + Matrix4x4 flipX = Matrix4x4::Scaling(-1, 1, 1); + Matrix4x4 flipY = Matrix4x4::Scaling(1, -1, 1); + + if (mIsCoveringTitlebar && !mIsFullscreen) { + // Mask the top corners. + mCornerMaskImage->Draw(aManager, aRect.TopLeft()); + mCornerMaskImage->Draw(aManager, aRect.TopRight(), flipX); + } + + if (mHasRoundedBottomCorners && !mIsFullscreen) { + // Mask the bottom corners. + mCornerMaskImage->Draw(aManager, aRect.BottomLeft(), flipY); + mCornerMaskImage->Draw(aManager, aRect.BottomRight(), flipY * flipX); + } + + // Reset blend mode. + aManager->gl()->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA, + LOCAL_GL_ONE, LOCAL_GL_ONE); +} + +static int32_t +FindTitlebarBottom(const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries, + int32_t aWindowWidth) +{ + int32_t titlebarBottom = 0; + for (uint32_t i = 0; i < aThemeGeometries.Length(); ++i) { + const nsIWidget::ThemeGeometry& g = aThemeGeometries[i]; + if ((g.mType == nsNativeThemeCocoa::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 == nsNativeThemeCocoa::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, nsNativeThemeCocoa::eThemeGeometryTypeToolbox).YMost(); + + ToolbarWindow* win = (ToolbarWindow*)[mView window]; + bool drawsContentsIntoWindowFrame = [win drawsContentsIntoWindowFrame]; + int32_t titlebarHeight = CocoaPointsToDevPixels([win titlebarHeight]); + int32_t contentOffset = drawsContentsIntoWindowFrame ? titlebarHeight : 0; + int32_t devUnifiedHeight = titlebarHeight + unifiedToolbarBottom - contentOffset; + [win setUnifiedToolbarHeight:DevPixelsToCocoaPoints(devUnifiedHeight)]; + int32_t devSheetPosition = titlebarHeight + std::max(toolboxBottom, unifiedToolbarBottom) - contentOffset; + [win setSheetAttachmentPosition:DevPixelsToCocoaPoints(devSheetPosition)]; + + // Update titlebar control offsets. + LayoutDeviceIntRect windowButtonRect = FindFirstRectOfType(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeWindowButtons); + [win placeWindowButtons:[mView convertRect:DevPixelsToCocoaPoints(windowButtonRect) toView:nil]]; + LayoutDeviceIntRect fullScreenButtonRect = FindFirstRectOfType(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeFullscreenButton); + [win placeFullScreenButton:[mView convertRect:DevPixelsToCocoaPoints(fullScreenButtonRect) toView:nil]]; +} + +static LayoutDeviceIntRegion +GatherThemeGeometryRegion(const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries, + nsITheme::ThemeGeometryType aThemeGeometryType) +{ + LayoutDeviceIntRegion region; + for (size_t i = 0; i < aThemeGeometries.Length(); ++i) { + const nsIWidget::ThemeGeometry& g = aThemeGeometries[i]; + if (g.mType == aThemeGeometryType) { + region.OrWith(g.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) +{ + if (!VibrancyManager::SystemSupportsVibrancy()) { + return; + } + + LayoutDeviceIntRegion sheetRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeSheet); + LayoutDeviceIntRegion vibrantLightRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeVibrancyLight); + LayoutDeviceIntRegion vibrantDarkRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeVibrancyDark); + LayoutDeviceIntRegion menuRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeMenu); + LayoutDeviceIntRegion tooltipRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeTooltip); + LayoutDeviceIntRegion highlightedMenuItemRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeHighlightedMenuItem); + LayoutDeviceIntRegion sourceListRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeSourceList); + LayoutDeviceIntRegion sourceListSelectionRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeSourceListSelection); + LayoutDeviceIntRegion activeSourceListSelectionRegion = + GatherThemeGeometryRegion(aThemeGeometries, nsNativeThemeCocoa::eThemeGeometryTypeActiveSourceListSelection); + + MakeRegionsNonOverlapping(sheetRegion, vibrantLightRegion, vibrantDarkRegion, + menuRegion, tooltipRegion, highlightedMenuItemRegion, + sourceListRegion, sourceListSelectionRegion, + activeSourceListSelectionRegion); + + auto& vm = EnsureVibrancyManager(); + vm.UpdateVibrantRegion(VibrancyType::LIGHT, vibrantLightRegion); + vm.UpdateVibrantRegion(VibrancyType::TOOLTIP, tooltipRegion); + vm.UpdateVibrantRegion(VibrancyType::MENU, menuRegion); + vm.UpdateVibrantRegion(VibrancyType::HIGHLIGHTED_MENUITEM, highlightedMenuItemRegion); + vm.UpdateVibrantRegion(VibrancyType::SHEET, sheetRegion); + vm.UpdateVibrantRegion(VibrancyType::SOURCE_LIST, sourceListRegion); + vm.UpdateVibrantRegion(VibrancyType::SOURCE_LIST_SELECTION, sourceListSelectionRegion); + vm.UpdateVibrantRegion(VibrancyType::ACTIVE_SOURCE_LIST_SELECTION, activeSourceListSelectionRegion); + vm.UpdateVibrantRegion(VibrancyType::DARK, vibrantDarkRegion); +} + +void +nsChildView::ClearVibrantAreas() +{ + if (VibrancyManager::SystemSupportsVibrancy()) { + EnsureVibrancyManager().ClearVibrantAreas(); + } +} + +static VibrancyType +ThemeGeometryTypeToVibrancyType(nsITheme::ThemeGeometryType aThemeGeometryType) +{ + switch (aThemeGeometryType) { + case nsNativeThemeCocoa::eThemeGeometryTypeVibrancyLight: + return VibrancyType::LIGHT; + case nsNativeThemeCocoa::eThemeGeometryTypeVibrancyDark: + return VibrancyType::DARK; + case nsNativeThemeCocoa::eThemeGeometryTypeTooltip: + return VibrancyType::TOOLTIP; + case nsNativeThemeCocoa::eThemeGeometryTypeMenu: + return VibrancyType::MENU; + case nsNativeThemeCocoa::eThemeGeometryTypeHighlightedMenuItem: + return VibrancyType::HIGHLIGHTED_MENUITEM; + case nsNativeThemeCocoa::eThemeGeometryTypeSheet: + return VibrancyType::SHEET; + case nsNativeThemeCocoa::eThemeGeometryTypeSourceList: + return VibrancyType::SOURCE_LIST; + case nsNativeThemeCocoa::eThemeGeometryTypeSourceListSelection: + return VibrancyType::SOURCE_LIST_SELECTION; + case nsNativeThemeCocoa::eThemeGeometryTypeActiveSourceListSelection: + return VibrancyType::ACTIVE_SOURCE_LIST_SELECTION; + default: + MOZ_CRASH(); + } +} + +NSColor* +nsChildView::VibrancyFillColorForThemeGeometryType(nsITheme::ThemeGeometryType aThemeGeometryType) +{ + if (VibrancyManager::SystemSupportsVibrancy()) { + return EnsureVibrancyManager().VibrancyFillColorForType( + ThemeGeometryTypeToVibrancyType(aThemeGeometryType)); + } + return [NSColor whiteColor]; +} + +NSColor* +nsChildView::VibrancyFontSmoothingBackgroundColorForThemeGeometryType(nsITheme::ThemeGeometryType aThemeGeometryType) +{ + if (VibrancyManager::SystemSupportsVibrancy()) { + return EnsureVibrancyManager().VibrancyFontSmoothingBackgroundColorForType( + ThemeGeometryTypeToVibrancyType(aThemeGeometryType)); + } + return [NSColor clearColor]; +} + +mozilla::VibrancyManager& +nsChildView::EnsureVibrancyManager() +{ + MOZ_ASSERT(mView, "Only call this once we have a view!"); + if (!mVibrancyManager) { + mVibrancyManager = MakeUnique<VibrancyManager>(*this, mView); + } + return *mVibrancyManager; +} + +nsChildView::SwipeInfo +nsChildView::SendMayStartSwipe(const mozilla::PanGestureInput& aSwipeStartEvent) +{ + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + + uint32_t direction = (aSwipeStartEvent.mPanDisplacement.x > 0.0) + ? (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT + : (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_LEFT; + + // We're ready to start the animation. Tell Gecko about it, and at the same + // time ask it if it really wants to start an animation for this event. + // This event also reports back the directions that we can swipe in. + LayoutDeviceIntPoint position = + RoundedToInt(aSwipeStartEvent.mPanStartPoint * ScreenToLayoutDeviceScale(1)); + WidgetSimpleGestureEvent geckoEvent = + SwipeTracker::CreateSwipeGestureEvent(eSwipeGestureMayStart, this, + position); + geckoEvent.mDirection = direction; + geckoEvent.mDelta = 0.0; + geckoEvent.mAllowedDirections = 0; + bool shouldStartSwipe = DispatchWindowEvent(geckoEvent); // event cancelled == swipe should start + + SwipeInfo result = { shouldStartSwipe, geckoEvent.mAllowedDirections }; + return result; +} + +void +nsChildView::TrackScrollEventAsSwipe(const mozilla::PanGestureInput& aSwipeStartEvent, + uint32_t aAllowedDirections) +{ + // If a swipe is currently being tracked kill it -- it's been interrupted + // by another gesture event. + if (mSwipeTracker) { + mSwipeTracker->CancelSwipe(); + mSwipeTracker->Destroy(); + mSwipeTracker = nullptr; + } + + uint32_t direction = (aSwipeStartEvent.mPanDisplacement.x > 0.0) + ? (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT + : (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_LEFT; + + mSwipeTracker = new SwipeTracker(*this, aSwipeStartEvent, + aAllowedDirections, direction); + + if (!mAPZC) { + mCurrentPanGestureBelongsToSwipe = true; + } +} + +void +nsChildView::SwipeFinished() +{ + mSwipeTracker = nullptr; +} + +already_AddRefed<gfx::DrawTarget> +nsChildView::StartRemoteDrawingInRegion(LayoutDeviceIntRegion& aInvalidRegion, + BufferMode* aBufferMode) +{ + // should have created the GLPresenter in InitCompositor. + MOZ_ASSERT(mGLPresenter); + if (!mGLPresenter) { + mGLPresenter = GLPresenter::CreateForWindow(this); + + if (!mGLPresenter) { + return nullptr; + } + } + + LayoutDeviceIntRegion dirtyRegion(aInvalidRegion); + LayoutDeviceIntSize renderSize = mBounds.Size(); + + if (!mBasicCompositorImage) { + mBasicCompositorImage = MakeUnique<RectTextureImage>(); + } + + RefPtr<gfx::DrawTarget> drawTarget = + mBasicCompositorImage->BeginUpdate(renderSize, dirtyRegion); + + if (!drawTarget) { + // Composite unchanged textures. + DoRemoteComposition(mBounds); + return nullptr; + } + + aInvalidRegion = mBasicCompositorImage->GetUpdateRegion(); + *aBufferMode = BufferMode::BUFFER_NONE; + + return drawTarget.forget(); +} + +void +nsChildView::EndRemoteDrawing() +{ + mBasicCompositorImage->EndUpdate(); + DoRemoteComposition(mBounds); +} + +void +nsChildView::CleanupRemoteDrawing() +{ + mBasicCompositorImage = nullptr; + mCornerMaskImage = nullptr; + mResizerImage = nullptr; + mTitlebarImage = nullptr; + mGLPresenter = nullptr; +} + +bool +nsChildView::InitCompositor(Compositor* aCompositor) +{ + if (aCompositor->GetBackendType() == LayersBackend::LAYERS_BASIC) { + if (!mGLPresenter) { + mGLPresenter = GLPresenter::CreateForWindow(this); + } + + return !!mGLPresenter; + } + return true; +} + +void +nsChildView::DoRemoteComposition(const LayoutDeviceIntRect& aRenderRect) +{ + if (![(ChildView*)mView preRender:mGLPresenter->GetNSOpenGLContext()]) { + return; + } + mGLPresenter->BeginFrame(aRenderRect.Size()); + + // Draw the result from the basic compositor. + mBasicCompositorImage->Draw(mGLPresenter.get(), LayoutDeviceIntPoint(0, 0)); + + // DrawWindowOverlay doesn't do anything for non-GL, so it didn't paint + // anything during the basic compositor transaction. Draw the overlay now. + DrawWindowOverlay(mGLPresenter.get(), aRenderRect); + + mGLPresenter->EndFrame(); + + [(ChildView*)mView postRender:mGLPresenter->GetNSOpenGLContext()]; +} + +@interface NonDraggableView : NSView +@end + +@implementation NonDraggableView +- (BOOL)mouseDownCanMoveWindow { return NO; } +- (NSView*)hitTest:(NSPoint)aPoint { return nil; } +@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, ^() { + 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]; + } +} + +void +nsChildView::ReportSwipeStarted(uint64_t aInputBlockId, + bool aStartSwipe) +{ + if (mSwipeEventQueue && mSwipeEventQueue->inputBlockId == aInputBlockId) { + if (aStartSwipe) { + PanGestureInput& startEvent = mSwipeEventQueue->queuedEvents[0]; + TrackScrollEventAsSwipe(startEvent, mSwipeEventQueue->allowedDirections); + for (size_t i = 1; i < mSwipeEventQueue->queuedEvents.Length(); i++) { + mSwipeTracker->ProcessEvent(mSwipeEventQueue->queuedEvents[i]); + } + } + mSwipeEventQueue = nullptr; + } +} + +void +nsChildView::DispatchAPZWheelInputEvent(InputData& aEvent, bool aCanTriggerSwipe) +{ + 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) { + uint64_t inputBlockId = 0; + ScrollableLayerGuid guid; + + nsEventStatus result = mAPZC->ReceiveInputEvent(aEvent, &guid, &inputBlockId); + if (result == nsEventStatus_eConsumeNoDefault) { + return; + } + + switch(aEvent.mInputType) { + case PANGESTURE_INPUT: { + PanGestureInput& panInput = aEvent.AsPanGestureInput(); + + event = panInput.ToWidgetWheelEvent(this); + if (aCanTriggerSwipe) { + SwipeInfo swipeInfo = SendMayStartSwipe(panInput); + event.mCanTriggerSwipe = swipeInfo.wantsSwipe; + if (swipeInfo.wantsSwipe) { + if (result == nsEventStatus_eIgnore) { + // APZ has determined and that scrolling horizontally in the + // requested direction is impossible, so it didn't do any + // scrolling for the event. + // We know now that MayStartSwipe wants a swipe, so we can start + // the swipe now. + TrackScrollEventAsSwipe(panInput, swipeInfo.allowedDirections); + } else { + // We don't know whether this event can start a swipe, so we need + // to queue up events and wait for a call to ReportSwipeStarted. + // APZ might already have started scrolling in response to the + // event if it knew that it's the right thing to do. In that case + // we'll still get a call to ReportSwipeStarted, and we will + // discard the queued events at that point. + mSwipeEventQueue = MakeUnique<SwipeEventQueue>(swipeInfo.allowedDirections, + inputBlockId); + } + } + } + + if (mSwipeEventQueue && mSwipeEventQueue->inputBlockId == inputBlockId) { + mSwipeEventQueue->queuedEvents.AppendElement(panInput); + } + break; + } + case SCROLLWHEEL_INPUT: { + event = aEvent.AsScrollWheelInput().ToWidgetWheelEvent(this); + break; + }; + default: + MOZ_CRASH("unsupported event type"); + return; + } + if (event.mMessage == eWheel && + (event.mDeltaX != 0 || event.mDeltaY != 0)) { + ProcessUntransformedAPZEvent(&event, guid, inputBlockId, result); + } + return; + } + + nsEventStatus status; + switch(aEvent.mInputType) { + case PANGESTURE_INPUT: { + PanGestureInput panInput = aEvent.AsPanGestureInput(); + if (panInput.mType == PanGestureInput::PANGESTURE_MAYSTART || + panInput.mType == PanGestureInput::PANGESTURE_START) { + mCurrentPanGestureBelongsToSwipe = false; + } + if (mCurrentPanGestureBelongsToSwipe) { + // Ignore this event. It's a momentum event from a scroll gesture + // that was processed as a swipe, and the swipe animation has + // already finished (so mSwipeTracker is already null). + MOZ_ASSERT(panInput.IsMomentum(), + "If the fingers are still on the touchpad, we should still have a SwipeTracker, and it should have consumed this event."); + return; + } + + event = panInput.ToWidgetWheelEvent(this); + if (aCanTriggerSwipe) { + SwipeInfo swipeInfo = SendMayStartSwipe(panInput); + + // We're in the non-APZ case here, but we still want to know whether + // the event was routed to a child process, so we use InputAPZContext + // to get that piece of information. + ScrollableLayerGuid guid; + InputAPZContext context(guid, 0, nsEventStatus_eIgnore); + + event.mCanTriggerSwipe = swipeInfo.wantsSwipe; + DispatchEvent(&event, status); + if (swipeInfo.wantsSwipe) { + if (context.WasRoutedToChildProcess()) { + // We don't know whether this event can start a swipe, so we need + // to queue up events and wait for a call to ReportSwipeStarted. + mSwipeEventQueue = MakeUnique<SwipeEventQueue>(swipeInfo.allowedDirections, 0); + } else if (event.TriggersSwipe()) { + TrackScrollEventAsSwipe(panInput, swipeInfo.allowedDirections); + } + } + + if (mSwipeEventQueue && mSwipeEventQueue->inputBlockId == 0) { + mSwipeEventQueue->queuedEvents.AppendElement(panInput); + } + return; + } + break; + } + case SCROLLWHEEL_INPUT: { + event = aEvent.AsScrollWheelInput().ToWidgetWheelEvent(this); + break; + } + default: + MOZ_CRASH("unexpected event type"); + return; + } + if (event.mMessage == eWheel && + (event.mDeltaX != 0 || event.mDeltaY != 0)) { + DispatchEvent(&event, status); + } +} + +// When using 10.11, calling showDefinitionForAttributedString causes the +// following exception on LookupViewService. (rdar://26476091) +// +// Exception: decodeObjectForKey: class "TitlebarAndBackgroundColor" not +// loaded or does not exist +// +// So we set temporary color that is NSColor before calling it. + +class MOZ_RAII AutoBackgroundSetter final { +public: + explicit AutoBackgroundSetter(NSView* aView) { + if (nsCocoaFeatures::OnElCapitanOrLater() && + [[aView window] isKindOfClass:[ToolbarWindow class]]) { + mWindow = [(ToolbarWindow*)[aView window] retain]; + [mWindow setTemporaryBackgroundColor]; + } else { + mWindow = nullptr; + } + } + + ~AutoBackgroundSetter() { + if (mWindow) { + [mWindow restoreBackgroundColor]; + [mWindow release]; + } + } + +private: + ToolbarWindow* mWindow; // strong +}; + +void +nsChildView::LookUpDictionary( + const nsAString& aText, + const nsTArray<mozilla::FontRange>& aFontRangeArray, + const bool aIsVertical, + const LayoutDeviceIntPoint& aPoint) +{ + NS_OBJC_BEGIN_TRY_ABORT_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]; + } + } + + AutoBackgroundSetter setter(mView); + [mView showDefinitionForAttributedString:attrStr atPoint:pt]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +#ifdef ACCESSIBILITY +already_AddRefed<a11y::Accessible> +nsChildView::GetDocumentAccessible() +{ + if (!mozilla::a11y::ShouldA11yBeEnabled()) + return nullptr; + + if (mAccessible) { + RefPtr<a11y::Accessible> ret; + CallQueryReferent(mAccessible.get(), + static_cast<a11y::Accessible**>(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::Accessible> acc = GetRootAccessible(); + mAccessible = do_GetWeakReference(acc.get()); + + return acc.forget(); +} +#endif + +// GLPresenter implementation + +GLPresenter::GLPresenter(GLContext* aContext) + : mGLContext(aContext) +{ + mGLContext->MakeCurrent(); + ShaderConfigOGL config; + config.SetTextureTarget(LOCAL_GL_TEXTURE_RECTANGLE_ARB); + mRGBARectProgram = MakeUnique<ShaderProgramOGL>(mGLContext, + ProgramProfileOGL::GetProfileFor(config)); + + // Create mQuadVBO. + mGLContext->fGenBuffers(1, &mQuadVBO); + mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO); + + // 1 quad, with the number of the quad (vertexID) encoded in w. + GLfloat vertices[] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 0.0f, + }; + HeapCopyOfStackArray<GLfloat> verticesOnHeap(vertices); + mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER, + verticesOnHeap.ByteLength(), + verticesOnHeap.Data(), + LOCAL_GL_STATIC_DRAW); + mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0); +} + +GLPresenter::~GLPresenter() +{ + if (mQuadVBO) { + mGLContext->MakeCurrent(); + mGLContext->fDeleteBuffers(1, &mQuadVBO); + mQuadVBO = 0; + } +} + +void +GLPresenter::BindAndDrawQuad(ShaderProgramOGL *aProgram, + const gfx::Rect& aLayerRect, + const gfx::Rect& aTextureRect) +{ + mGLContext->MakeCurrent(); + + gfx::Rect layerRects[4]; + gfx::Rect textureRects[4]; + + layerRects[0] = aLayerRect; + textureRects[0] = aTextureRect; + + aProgram->SetLayerRects(layerRects); + aProgram->SetTextureRects(textureRects); + + const GLuint coordAttribIndex = 0; + + mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO); + mGLContext->fVertexAttribPointer(coordAttribIndex, 4, + LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0, + (GLvoid*)0); + mGLContext->fEnableVertexAttribArray(coordAttribIndex); + mGLContext->fDrawArrays(LOCAL_GL_TRIANGLES, 0, 6); + mGLContext->fDisableVertexAttribArray(coordAttribIndex); +} + +void +GLPresenter::BeginFrame(LayoutDeviceIntSize aRenderSize) +{ + mGLContext->MakeCurrent(); + + mGLContext->fViewport(0, 0, aRenderSize.width, aRenderSize.height); + + // Matrix to transform (0, 0, width, height) to viewport space (-1.0, 1.0, + // 2, 2) and flip the contents. + gfx::Matrix viewMatrix = gfx::Matrix::Translation(-1.0, 1.0); + viewMatrix.PreScale(2.0f / float(aRenderSize.width), + 2.0f / float(aRenderSize.height)); + viewMatrix.PreScale(1.0f, -1.0f); + + gfx::Matrix4x4 matrix3d = gfx::Matrix4x4::From2D(viewMatrix); + matrix3d._33 = 0.0f; + + // set the projection matrix for the next time the program is activated + mProjMatrix = matrix3d; + + // Default blend function implements "OVER" + mGLContext->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA, + LOCAL_GL_ONE, LOCAL_GL_ONE); + mGLContext->fEnable(LOCAL_GL_BLEND); + + mGLContext->fClearColor(0.0, 0.0, 0.0, 0.0); + mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT); + + mGLContext->fEnable(LOCAL_GL_TEXTURE_RECTANGLE_ARB); +} + +void +GLPresenter::EndFrame() +{ + mGLContext->SwapBuffers(); + mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0); +} + +class WidgetsReleaserRunnable final : public mozilla::Runnable +{ +public: + explicit WidgetsReleaserRunnable(nsTArray<nsCOMPtr<nsIWidget>>&& aWidgetArray) + : mWidgetArray(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 - + +@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 mouseDragged is on the call stack. +NSView* gLastDragView = nil; +NSEvent* gLastDragMouseDownEvent = nil; + ++ (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 *sendTypes = [[NSArray alloc] initWithObjects:NSStringPboardType,NSHTMLPboardType,nil]; + NSArray *returnTypes = [[NSArray alloc] initWithObjects:NSStringPboardType,NSHTMLPboardType,nil]; + + [NSApp registerServicesMenuSendTypes:sendTypes returnTypes:returnTypes]; + + [sendTypes release]; + [returnTypes release]; + + initialized = YES; + } +} + ++ (void)registerViewForDraggedTypes:(NSView*)aView +{ + [aView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, + NSStringPboardType, + NSHTMLPboardType, + NSURLPboardType, + NSFilesPromisePboardType, + kWildcardPboardType, + kCorePboardType_url, + kCorePboardType_urld, + kCorePboardType_urln, + nil]]; +} + +// initWithFrame:geckoChild: +- (id)initWithFrame:(NSRect)inFrame geckoChild:(nsChildView*)inChild +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ((self = [super initWithFrame:inFrame])) { + mGeckoChild = inChild; + mPendingDisplay = NO; + mBlockedLastMouseDown = NO; + mExpectingWheelStop = NO; + + mLastMouseDownEvent = nil; + mLastKeyDownEvent = nil; + mClickThroughMouseDownEvent = nil; + mDragService = nullptr; + + mGestureState = eGestureState_None; + mCumulativeMagnification = 0.0; + mCumulativeRotation = 0.0; + + mNeedsGLUpdate = NO; + + [self setFocusRingType:NSFocusRingTypeNone]; + +#ifdef __LP64__ + mCancelSwipeAnimation = nil; +#endif + + mTopLeftCornerMask = NULL; + } + + // register for things we'll take from other applications + [ChildView registerViewForDraggedTypes:self]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(systemMetricsChanged) + name:NSControlTintDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(systemMetricsChanged) + name:NSSystemColorsDidChangeNotification + object:nil]; + // TODO: replace the string with the constant once we build with the 10.7 SDK + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(scrollbarSystemMetricChanged) + name:@"NSPreferredScrollerStyleDidChangeNotification" + object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(systemMetricsChanged) + name:@"AppleAquaScrollBarVariantChanged" + object:nil + suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_surfaceNeedsUpdate:) + name:NSViewGlobalFrameDidChangeNotification + object:self]; + + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// ComplexTextInputPanel's interpretKeyEvent hack won't work without this. +// It makes calls to +[NSTextInputContext currentContext], deep in system +// code, return the appropriate context. +- (NSTextInputContext *)inputContext +{ + NSTextInputContext* pluginContext = NULL; + if (mGeckoChild && mGeckoChild->IsPluginFocused()) { + ComplexTextInputPanel* ctiPanel = + ComplexTextInputPanel::GetSharedComplexTextInputPanel(); + if (ctiPanel) { + pluginContext = (NSTextInputContext*) ctiPanel->GetInputContext(); + } + } + if (pluginContext) { + return pluginContext; + } else { + 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; +} + +- (bool)preRender:(NSOpenGLContext *)aGLContext +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (![self window] || + ([[self window] isKindOfClass:[BaseWindow class]] && + ![(BaseWindow*)[self window] isVisibleOrBeingShown])) { + // Before the window is shown, our GL context's front FBO is not + // framebuffer complete, so we refuse to render. + return false; + } + + if (!mGLContext) { + mGLContext = aGLContext; + [mGLContext retain]; + mNeedsGLUpdate = true; + } + + CGLLockContext((CGLContextObj)[aGLContext CGLContextObj]); + + if (mNeedsGLUpdate) { + [self updateGLContext]; + mNeedsGLUpdate = NO; + } + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +- (void)postRender:(NSOpenGLContext *)aGLContext +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + CGLUnlockContext((CGLContextObj)[aGLContext CGLContextObj]); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mGLContext release]; + [mPendingDirtyRects release]; + [mLastMouseDownEvent release]; + [mLastKeyDownEvent release]; + [mClickThroughMouseDownEvent release]; + CGImageRelease(mTopLeftCornerMask); + ChildViewMouseTracker::OnDestroyView(self); + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; + + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_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); +} + +- (void)systemMetricsChanged +{ + if (mGeckoChild) + mGeckoChild->NotifyThemeChanged(); +} + +- (void)scrollbarSystemMetricChanged +{ + [self systemMetricsChanged]; + + if (mGeckoChild) { + nsIWidgetListener* listener = mGeckoChild->GetWidgetListener(); + if (listener) { + nsIPresShell* presShell = listener->GetPresShell(); + if (presShell) { + presShell->ReconstructFrames(); + } + } + } +} + +- (void)setNeedsPendingDisplay +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mPendingFullDisplay = YES; + if (!mPendingDisplay) { + [self performSelector:@selector(processPendingRedraws) withObject:nil afterDelay:0]; + mPendingDisplay = YES; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)setNeedsPendingDisplayInRect:(NSRect)invalidRect +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mPendingDirtyRects) + mPendingDirtyRects = [[NSMutableArray alloc] initWithCapacity:1]; + [mPendingDirtyRects addObject:[NSValue valueWithRect:invalidRect]]; + if (!mPendingDisplay) { + [self performSelector:@selector(processPendingRedraws) withObject:nil afterDelay:0]; + mPendingDisplay = YES; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Clears the queue of any pending invalides +- (void)processPendingRedraws +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mPendingFullDisplay) { + [self setNeedsDisplay:YES]; + } + else if (mPendingDirtyRects) { + unsigned int count = [mPendingDirtyRects count]; + for (unsigned int i = 0; i < count; ++i) { + [self setNeedsDisplayInRect:[[mPendingDirtyRects objectAtIndex:i] rectValue]]; + } + } + mPendingFullDisplay = NO; + mPendingDisplay = NO; + [mPendingDirtyRects release]; + mPendingDirtyRects = nil; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)setNeedsDisplayInRect:(NSRect)aRect +{ + if (![self isUsingOpenGL]) { + [super setNeedsDisplayInRect:aRect]; + return; + } + + if ([[self window] isVisible] && [self isUsingMainThreadOpenGL]) { + // Draw without calling drawRect. This prevent us from + // needing to access the normal window buffer surface unnecessarily, so we + // waste less time synchronizing the two surfaces. (These synchronizations + // show up in a profiler as CGSDeviceLock / _CGSLockWindow / + // _CGSSynchronizeWindowBackingStore.) It also means that Cocoa doesn't + // have any potentially expensive invalid rect management for us. + if (!mWaitingForPaint) { + mWaitingForPaint = YES; + // Use NSRunLoopCommonModes instead of the default NSDefaultRunLoopMode + // so that the timer also fires while a native menu is open. + [self performSelector:@selector(drawUsingOpenGLCallback) + withObject:nil + afterDelay:0 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } + } +} + +- (NSString*)description +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [NSString stringWithFormat:@"ChildView %p, gecko child %p, frame %@", self, mGeckoChild, NSStringFromRect([self frame])]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// Make the origin of this view the topLeft corner (gecko origin) rather +// than the bottomLeft corner (standard cocoa origin). +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)isOpaque +{ + return [[self window] isOpaque]; +} + +- (void)sendFocusEvent:(EventMessage)eventMessage +{ + if (!mGeckoChild) + return; + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetGUIEvent focusGuiEvent(true, eventMessage, mGeckoChild); + focusGuiEvent.mTime = PR_IntervalNow(); + mGeckoChild->DispatchEvent(&focusGuiEvent, status); +} + +// 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; +} + +- (void)scrollRect:(NSRect)aRect by:(NSSize)offset +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Update any pending dirty rects to reflect the new scroll position + if (mPendingDirtyRects) { + unsigned int count = [mPendingDirtyRects count]; + for (unsigned int i = 0; i < count; ++i) { + NSRect oldRect = [[mPendingDirtyRects objectAtIndex:i] rectValue]; + NSRect newRect = NSOffsetRect(oldRect, offset.width, offset.height); + [mPendingDirtyRects replaceObjectAtIndex:i + withObject:[NSValue valueWithRect:newRect]]; + } + } + [super scrollRect:aRect by:offset]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (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)updateGLContext +{ + [mGLContext setView:self]; + [mGLContext update]; +} + +- (void)_surfaceNeedsUpdate:(NSNotification*)notification +{ + if (mGLContext) { + CGLLockContext((CGLContextObj)[mGLContext CGLContextObj]); + mNeedsGLUpdate = YES; + CGLUnlockContext((CGLContextObj)[mGLContext CGLContextObj]); + } +} + +- (BOOL)wantsBestResolutionOpenGLSurface +{ + return nsCocoaUtils::HiDPIEnabled() ? YES : NO; +} + +- (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 +{ + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + + if (!observerService) { + return; + } + + observerService->NotifyObservers(nullptr, "live-resize-start", nullptr); +} + +- (void)viewDidEndLiveResize +{ + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + + if (!observerService) { + return; + } + + observerService->NotifyObservers(nullptr, "live-resize-end", nullptr); +} + +- (NSColor*)vibrancyFillColorForThemeGeometryType:(nsITheme::ThemeGeometryType)aThemeGeometryType +{ + if (!mGeckoChild) { + return [NSColor whiteColor]; + } + return mGeckoChild->VibrancyFillColorForThemeGeometryType(aThemeGeometryType); +} + +- (NSColor*)vibrancyFontSmoothingBackgroundColorForThemeGeometryType:(nsITheme::ThemeGeometryType)aThemeGeometryType +{ + if (!mGeckoChild) { + return [NSColor clearColor]; + } + return mGeckoChild->VibrancyFontSmoothingBackgroundColorForThemeGeometryType(aThemeGeometryType); +} + +- (LayoutDeviceIntRegion)nativeDirtyRegionWithBoundingRect:(NSRect)aRect +{ + LayoutDeviceIntRect boundingRect = mGeckoChild->CocoaPointsToDevPixels(aRect); + const NSRect *rects; + NSInteger count; + [self getRectsBeingDrawn:&rects count:&count]; + + if (count > MAX_RECTS_IN_REGION) { + return boundingRect; + } + + LayoutDeviceIntRegion region; + for (NSInteger i = 0; i < count; ++i) { + region.Or(region, mGeckoChild->CocoaPointsToDevPixels(rects[i])); + } + region.And(region, boundingRect); + return region; +} + +// The display system has told us that a portion of our view is dirty. Tell +// gecko to paint it +- (void)drawRect:(NSRect)aRect +{ + CGContextRef cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + [self drawRect:aRect inContext:cgContext]; + + // If we're a transparent window and our contents have changed, we need + // to make sure the shadow is updated to the new contents. + if ([[self window] isKindOfClass:[BaseWindow class]]) { + [(BaseWindow*)[self window] deferredInvalidateShadow]; + } +} + +- (void)drawRect:(NSRect)aRect inContext:(CGContextRef)aContext +{ + if (!mGeckoChild || !mGeckoChild->IsVisible()) + return; + +#ifdef DEBUG_UPDATE + LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds(); + + fprintf (stderr, "---- Update[%p][%p] [%f %f %f %f] cgc: %p\n gecko bounds: [%d %d %d %d]\n", + self, mGeckoChild, + aRect.origin.x, aRect.origin.y, aRect.size.width, aRect.size.height, aContext, + geckoBounds.x, geckoBounds.y, geckoBounds.width, geckoBounds.height); + + CGAffineTransform xform = CGContextGetCTM(aContext); + fprintf (stderr, " xform in: [%f %f %f %f %f %f]\n", xform.a, xform.b, xform.c, xform.d, xform.tx, xform.ty); +#endif + + if ([self isUsingOpenGL]) { + // For Gecko-initiated repaints in OpenGL mode, drawUsingOpenGL is + // directly called from a delayed perform callback - without going through + // drawRect. + // Paints that come through here are triggered by something that Cocoa + // controls, for example by window resizing or window focus changes. + + // Since this view is usually declared as opaque, the window's pixel + // buffer may now contain garbage which we need to prevent from reaching + // the screen. The only place where garbage can show is in the window + // corners and the vibrant regions of the window - the rest of the window + // is covered by opaque content in our OpenGL surface. + // So we need to clear the pixel buffer contents in these areas. + mGeckoChild->ClearVibrantAreas(); + [self clearCorners]; + + // Do GL composition and return. + [self drawUsingOpenGL]; + return; + } + + PROFILER_LABEL("ChildView", "drawRect", + js::ProfileEntry::Category::GRAPHICS); + + // The CGContext that drawRect supplies us with comes with a transform that + // scales one user space unit to one Cocoa point, which can consist of + // multiple dev pixels. But Gecko expects its supplied context to be scaled + // to device pixels, so we need to reverse the scaling. + double scale = mGeckoChild->BackingScaleFactor(); + CGContextSaveGState(aContext); + CGContextScaleCTM(aContext, 1.0 / scale, 1.0 / scale); + + NSSize viewSize = [self bounds].size; + gfx::IntSize backingSize = gfx::IntSize::Truncate(viewSize.width * scale, viewSize.height * scale); + LayoutDeviceIntRegion region = [self nativeDirtyRegionWithBoundingRect:aRect]; + + bool painted = mGeckoChild->PaintWindowInContext(aContext, region, backingSize); + + // Undo the scale transform so that from now on the context is in + // CocoaPoints again. + CGContextRestoreGState(aContext); + + if (!painted && [self isOpaque]) { + // Gecko refused to draw, but we've claimed to be opaque, so we have to + // draw something--fill with white. + CGContextSetRGBFillColor(aContext, 1, 1, 1, 1); + CGContextFillRect(aContext, NSRectToCGRect(aRect)); + } + + if ([self isCoveringTitlebar]) { + [self drawTitleString]; + [self drawTitlebarHighlight]; + [self maskTopCornersInContext:aContext]; + } + +#ifdef DEBUG_UPDATE + fprintf (stderr, "---- update done ----\n"); + +#if 0 + CGContextSetRGBStrokeColor (aContext, + ((((unsigned long)self) & 0xff)) / 255.0, + ((((unsigned long)self) & 0xff00) >> 8) / 255.0, + ((((unsigned long)self) & 0xff0000) >> 16) / 255.0, + 0.5); +#endif + CGContextSetRGBStrokeColor(aContext, 1, 0, 0, 0.8); + CGContextSetLineWidth(aContext, 4.0); + CGContextStrokeRect(aContext, NSRectToCGRect(aRect)); +#endif +} + +- (BOOL)isUsingMainThreadOpenGL +{ + if (!mGeckoChild || ![self window]) + return NO; + + return mGeckoChild->GetLayerManager(nullptr)->GetBackendType() == mozilla::layers::LayersBackend::LAYERS_OPENGL; +} + +- (BOOL)isUsingOpenGL +{ + if (!mGeckoChild || ![self window]) + return NO; + + return mGLContext || mUsingOMTCompositor || [self isUsingMainThreadOpenGL]; +} + +- (void)drawUsingOpenGL +{ + PROFILER_LABEL("ChildView", "drawUsingOpenGL", + js::ProfileEntry::Category::GRAPHICS); + + if (![self isUsingOpenGL] || !mGeckoChild->IsVisible()) + return; + + mWaitingForPaint = NO; + + LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds(); + LayoutDeviceIntRegion region(geckoBounds); + + mGeckoChild->PaintWindow(region); +} + +// Called asynchronously after setNeedsDisplay in order to avoid entering the +// normal drawing machinery. +- (void)drawUsingOpenGLCallback +{ + if (mWaitingForPaint) { + [self drawUsingOpenGL]; + } +} + +- (BOOL)hasRoundedBottomCorners +{ + return [[self window] respondsToSelector:@selector(bottomCornerRounded)] && + [[self window] bottomCornerRounded]; +} + +- (CGFloat)cornerRadius +{ + NSView* frameView = [[[self window] contentView] superview]; + if (!frameView || ![frameView respondsToSelector:@selector(roundedCornerRadius)]) + return 4.0f; + return [frameView roundedCornerRadius]; +} + +-(void)setGLOpaque:(BOOL)aOpaque +{ + CGLLockContext((CGLContextObj)[mGLContext CGLContextObj]); + // Make the context opaque for fullscreen (since it performs better), and transparent + // for windowed (since we need it for rounded corners). + GLint opaque = aOpaque ? 1 : 0; + [mGLContext setValues:&opaque forParameter:NSOpenGLCPSurfaceOpacity]; + CGLUnlockContext((CGLContextObj)[mGLContext CGLContextObj]); +} + +// Accelerated windows have two NSSurfaces: +// (1) The window's pixel buffer in the back and +// (2) the OpenGL view in the front. +// These two surfaces are composited by the window manager. Drawing into the +// CGContext which is provided by drawRect ends up in (1). +// When our window has rounded corners, the OpenGL view has transparent pixels +// in the corners. In these places the contents of the window's pixel buffer +// can show through. So we need to make sure that the pixel buffer is +// transparent in the corners so that no garbage reaches the screen. +// The contents of the pixel buffer in the rest of the window don't matter +// because they're covered by opaque pixels of the OpenGL context. +// Making the corners transparent works even though our window is +// declared "opaque" (in the NSWindow's isOpaque method). +- (void)clearCorners +{ + CGFloat radius = [self cornerRadius]; + CGFloat w = [self bounds].size.width, h = [self bounds].size.height; + [[NSColor clearColor] set]; + + if ([self isCoveringTitlebar]) { + NSRectFill(NSMakeRect(0, 0, radius, radius)); + NSRectFill(NSMakeRect(w - radius, 0, radius, radius)); + } + + if ([self hasRoundedBottomCorners]) { + NSRectFill(NSMakeRect(0, h - radius, radius, radius)); + NSRectFill(NSMakeRect(w - radius, h - radius, radius, radius)); + } +} + +// This is the analog of nsChildView::MaybeDrawRoundedCorners for CGContexts. +// We only need to mask the top corners here because Cocoa does the masking +// for the window's bottom corners automatically (starting with 10.7). +- (void)maskTopCornersInContext:(CGContextRef)aContext +{ + CGFloat radius = [self cornerRadius]; + int32_t devPixelCornerRadius = mGeckoChild->CocoaPointsToDevPixels(radius); + + // First make sure that mTopLeftCornerMask is set up. + if (!mTopLeftCornerMask || + int32_t(CGImageGetWidth(mTopLeftCornerMask)) != devPixelCornerRadius) { + CGImageRelease(mTopLeftCornerMask); + CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); + CGContextRef imgCtx = CGBitmapContextCreate(NULL, + devPixelCornerRadius, + devPixelCornerRadius, + 8, devPixelCornerRadius * 4, + rgb, kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(rgb); + DrawTopLeftCornerMask(imgCtx, devPixelCornerRadius); + mTopLeftCornerMask = CGBitmapContextCreateImage(imgCtx); + CGContextRelease(imgCtx); + } + + // kCGBlendModeDestinationIn is the secret sauce which allows us to erase + // already painted pixels. It's defined as R = D * Sa: multiply all channels + // of the destination pixel with the alpha of the source pixel. In our case, + // the source is mTopLeftCornerMask. + CGContextSaveGState(aContext); + CGContextSetBlendMode(aContext, kCGBlendModeDestinationIn); + + CGRect destRect = CGRectMake(0, 0, radius, radius); + + // Erase the top left corner... + CGContextDrawImage(aContext, destRect, mTopLeftCornerMask); + + // ... and the top right corner. + CGContextTranslateCTM(aContext, [self bounds].size.width, 0); + CGContextScaleCTM(aContext, -1, 1); + CGContextDrawImage(aContext, destRect, mTopLeftCornerMask); + + CGContextRestoreGState(aContext); +} + +- (void)drawTitleString +{ + BaseWindow* window = (BaseWindow*)[self window]; + if (![window wantsTitleDrawn]) { + return; + } + + NSView* frameView = [[window contentView] superview]; + if (![frameView respondsToSelector:@selector(_drawTitleBar:)]) { + return; + } + + NSGraphicsContext* oldContext = [NSGraphicsContext currentContext]; + CGContextRef ctx = (CGContextRef)[oldContext graphicsPort]; + CGContextSaveGState(ctx); + if ([oldContext isFlipped] != [frameView isFlipped]) { + CGContextTranslateCTM(ctx, 0, [self bounds].size.height); + CGContextScaleCTM(ctx, 1, -1); + } + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:[frameView isFlipped]]]; + [frameView _drawTitleBar:[frameView bounds]]; + CGContextRestoreGState(ctx); + [NSGraphicsContext setCurrentContext:oldContext]; +} + +- (void)drawTitlebarHighlight +{ + DrawTitlebarHighlight([self bounds].size, [self cornerRadius], + mGeckoChild->DevPixelsToCocoaPoints(1)); +} + +- (void)viewWillDraw +{ + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + if (mGeckoChild) { + // The OS normally *will* draw our NSWindow, no matter what we do here. + // But Gecko can delete our parent widget(s) (along with mGeckoChild) + // while processing a paint request, which closes our NSWindow and + // makes the OS throw an NSInternalInconsistencyException assertion when + // it tries to draw it. Sometimes the OS also aborts the browser process. + // So we need to retain our parent(s) here and not release it/them until + // the next time through the main thread's run loop. When we do this we + // also need to retain and release mGeckoChild, which holds a strong + // reference to us. See bug 550392. + nsIWidget* parent = mGeckoChild->GetParent(); + if (parent) { + nsTArray<nsCOMPtr<nsIWidget>> widgetArray; + while (parent) { + widgetArray.AppendElement(parent); + parent = parent->GetParent(); + } + widgetArray.AppendElement(mGeckoChild); + nsCOMPtr<nsIRunnable> releaserRunnable = + new WidgetsReleaserRunnable(Move(widgetArray)); + NS_DispatchToMainThread(releaserRunnable); + } + + if ([self isUsingOpenGL]) { + if (ShadowLayerForwarder* slf = mGeckoChild->GetLayerManager()->AsShadowForwarder()) { + slf->WindowOverlayChanged(); + } + } + + mGeckoChild->WillPaintWindow(); + } + [super viewWillDraw]; +} + +#if USE_CLICK_HOLD_CONTEXTMENU +// +// -clickHoldCallback: +// +// called from a timer two seconds after a mouse down to see if we should display +// a context menu (click-hold). |anEvent| is the original mouseDown event. If we're +// still in that mouseDown by this time, put up the context menu, otherwise just +// fuhgeddaboutit. |anEvent| has been retained by the OS until after this callback +// fires so we're ok there. +// +// This code currently messes in a bunch of edge cases (bugs 234751, 232964, 232314) +// so removing it until we get it straightened out. +// +- (void)clickHoldCallback:(id)theEvent; +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if( theEvent == [NSApp currentEvent] ) { + // we're still in the middle of the same mousedown event here, activate + // click-hold context menu by triggering the right mouseDown action. + NSEvent* clickHoldEvent = [NSEvent mouseEventWithType:NSRightMouseDown + location:[theEvent locationInWindow] + modifierFlags:[theEvent modifierFlags] + timestamp:[theEvent timestamp] + windowNumber:[theEvent windowNumber] + context:[theEvent context] + eventNumber:[theEvent eventNumber] + clickCount:[theEvent clickCount] + pressure:[theEvent pressure]]; + [self rightMouseDown:clickHoldEvent]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} +#endif + +// 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_ABORT_BLOCK; + +#ifdef MOZ_USE_NATIVE_POPUP_WINDOWS + return; +#endif /* MOZ_USE_NATIVE_POPUP_WINDOWS */ + + 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_ABORT_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_ABORT_BLOCK_RETURN; + + BOOL consumeEvent = NO; + + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + NS_ENSURE_TRUE(rollupListener, false); + 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 events should roll up the popup + if ([theEvent type] == NSScrollWheel) { + 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] == NSLeftMouseDown) { + NSPoint point = [NSEvent mouseLocation]; + FlipCocoaScreenCoordinate(point); + gfx::IntPoint pos = gfx::IntPoint::Truncate(point.x, point.y); + consumeEvent = (BOOL)rollupListener->Rollup(popupsToRollup, true, &pos, nullptr); + } + else { + consumeEvent = (BOOL)rollupListener->Rollup(popupsToRollup, true, nullptr, nullptr); + } + } + } + } + + return consumeEvent; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +/* + * In OS X Mountain Lion and above, smart zoom gestures are implemented in + * smartMagnifyWithEvent. In OS X Lion, they are implemented in + * magnifyWithEvent. See inline comments for more info. + * + * The prototypes swipeWithEvent, beginGestureWithEvent, magnifyWithEvent, + * 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 +{ + NS_OBJC_BEGIN_TRY_ABORT_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 |= nsIDOMSimpleGestureEvent::DIRECTION_LEFT; + else if (deltaX < 0.0) + geckoEvent.mDirection |= nsIDOMSimpleGestureEvent::DIRECTION_RIGHT; + + // Record the up/down direction. + if (deltaY > 0.0) + geckoEvent.mDirection |= nsIDOMSimpleGestureEvent::DIRECTION_UP; + else if (deltaY < 0.0) + geckoEvent.mDirection |= nsIDOMSimpleGestureEvent::DIRECTION_DOWN; + + // Send the event. + mGeckoChild->DispatchWindowEvent(geckoEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)beginGestureWithEvent:(NSEvent *)anEvent +{ + if (!anEvent) + return; + + mGestureState = eGestureState_StartGesture; + mCumulativeMagnification = 0; + mCumulativeRotation = 0.0; +} + +- (void)magnifyWithEvent:(NSEvent *)anEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!anEvent || !mGeckoChild) + return; + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + float deltaZ = [anEvent deltaZ]; + + EventMessage msg; + switch (mGestureState) { + case eGestureState_StartGesture: + msg = eMagnifyGestureStart; + mGestureState = eGestureState_MagnifyGesture; + break; + + case eGestureState_MagnifyGesture: + msg = eMagnifyGestureUpdate; + break; + + case eGestureState_None: + case eGestureState_RotateGesture: + default: + return; + } + + // Setup the event. + WidgetSimpleGestureEvent geckoEvent(true, msg, mGeckoChild); + geckoEvent.mDelta = deltaZ; + [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent]; + + // Send the event. + mGeckoChild->DispatchWindowEvent(geckoEvent); + + // Keep track of the cumulative magnification for the final "magnify" event. + mCumulativeMagnification += deltaZ; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)smartMagnifyWithEvent:(NSEvent *)anEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!anEvent || !mGeckoChild) { + return; + } + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + // 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_ABORT_BLOCK; +} + +- (void)rotateWithEvent:(NSEvent *)anEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!anEvent || !mGeckoChild) + 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 = nsIDOMSimpleGestureEvent::ROTATION_COUNTERCLOCKWISE; + } else { + geckoEvent.mDirection = nsIDOMSimpleGestureEvent::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_ABORT_BLOCK; +} + +- (void)endGestureWithEvent:(NSEvent *)anEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!anEvent || !mGeckoChild) { + // Clear the gestures state if we cannot send an event. + mGestureState = eGestureState_None; + mCumulativeMagnification = 0.0; + mCumulativeRotation = 0.0; + return; + } + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + switch (mGestureState) { + case eGestureState_MagnifyGesture: + { + // Setup the "magnify" event. + WidgetSimpleGestureEvent geckoEvent(true, eMagnifyGesture, mGeckoChild); + geckoEvent.mDelta = mCumulativeMagnification; + [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent]; + + // Send the event. + mGeckoChild->DispatchWindowEvent(geckoEvent); + } + break; + + 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 = nsIDOMSimpleGestureEvent::ROTATION_COUNTERCLOCKWISE; + } else { + geckoEvent.mDirection = nsIDOMSimpleGestureEvent::ROTATION_CLOCKWISE; + } + + // Send the event. + mGeckoChild->DispatchWindowEvent(geckoEvent); + } + break; + + case eGestureState_None: + case eGestureState_StartGesture: + default: + break; + } + + // Clear the gestures state. + mGestureState = eGestureState_None; + mCumulativeMagnification = 0.0; + mCumulativeRotation = 0.0; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (bool)shouldConsiderStartingSwipeFromEvent:(NSEvent*)anEvent +{ + // This method 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'. + if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) { + return false; + } + + // 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. + NSEventPhase eventPhase = nsCocoaUtils::EventPhase(anEvent); + if ([anEvent type] != NSScrollWheel || + eventPhase != NSEventPhaseBegan || + ![anEvent hasPreciseScrollingDeltas]) { + return false; + } + + // Only initiate horizontal tracking for events whose horizontal element is + // at least eight times larger than its vertical element. This minimizes + // performance problems with vertical scrolls (by minimizing the possibility + // that they'll be misinterpreted as horizontal swipes), while still + // tolerating a small vertical element to a true horizontal swipe. The number + // '8' was arrived at by trial and error. + CGFloat deltaX = [anEvent scrollingDeltaX]; + CGFloat deltaY = [anEvent scrollingDeltaY]; + return std::abs(deltaX) > std::abs(deltaY) * 8; +} + +- (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_ABORT_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]; + + // 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; + } + +#if USE_CLICK_HOLD_CONTEXTMENU + // fire off timer to check for click-hold after two seconds. retains |theEvent| + [self performSelector:@selector(clickHoldCallback:) withObject:theEvent afterDelay:2.0]; +#endif + + // in order to send gecko events we'll need a gecko widget + if (!mGeckoChild) + return; + if (mTextInputHandler->OnHandleEvent(theEvent)) { + return; + } + + NSUInteger modifierFlags = [theEvent modifierFlags]; + + 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 (modifierFlags & NSControlKeyMask) + geckoEvent.button = WidgetMouseEvent::eRightButton; + else + geckoEvent.button = WidgetMouseEvent::eLeftButton; + + mGeckoChild->DispatchInputEvent(&geckoEvent); + mBlockedLastMouseDown = NO; + + // XXX maybe call markedTextSelectionChanged:client: here? + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)mouseUp:(NSEvent *)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + 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 ([theEvent modifierFlags] & NSControlKeyMask) + geckoEvent.button = WidgetMouseEvent::eRightButton; + else + geckoEvent.button = WidgetMouseEvent::eLeftButton; + + // This might destroy our widget (and null out mGeckoChild). + bool defaultPrevented = + (mGeckoChild->DispatchInputEvent(&geckoEvent) == nsEventStatus_eConsumeNoDefault); + + // Check to see if we are double-clicking in the titlebar. + CGFloat locationInTitlebar = [[self window] frame].size.height - [theEvent locationInWindow].y; + LayoutDeviceIntPoint pos = geckoEvent.mRefPoint; + if (!defaultPrevented && [theEvent clickCount] == 2 && + !mGeckoChild->GetNonDraggableRegion().Contains(pos.x, pos.y) && + [[self window] isKindOfClass:[ToolbarWindow class]] && + (locationInTitlebar < [(ToolbarWindow*)[self window] titlebarHeight] || + locationInTitlebar < [(ToolbarWindow*)[self window] unifiedToolbarHeight])) { + if ([self shouldZoomOnDoubleClick]) { + [[self window] performZoom:nil]; + } else if ([self shouldMinimizeOnTitlebarDoubleClick]) { + NSButton *minimizeButton = [[self window] standardWindowButton:NSWindowMiniaturizeButton]; + [minimizeButton performClick:self]; + } + } + + NS_OBJC_END_TRY_ABORT_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); + + event.mExitFrom = aExitFrom; + + nsEventStatus status; // ignored + mGeckoChild->DispatchEvent(&event, status); +} + +- (void)handleMouseMoved:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +- (void)mouseDragged:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mGeckoChild) + return; + if (mTextInputHandler->OnHandleEvent(theEvent)) { + return; + } + + gLastDragView = self; + + 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. + gLastDragView = nil; + + // XXX maybe call markedTextSelectionChanged:client: here? + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)rightMouseDown:(NSEvent *)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_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.button = WidgetMouseEvent::eRightButton; + geckoEvent.mClickCount = [theEvent clickCount]; + + mGeckoChild->DispatchInputEvent(&geckoEvent); + if (!mGeckoChild) + return; + + // Let the superclass do the context menu stuff. + [super rightMouseDown:theEvent]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)rightMouseUp:(NSEvent *)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mGeckoChild) + return; + if (mTextInputHandler->OnHandleEvent(theEvent)) { + return; + } + + WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild, + WidgetMouseEvent::eReal); + [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent]; + geckoEvent.button = WidgetMouseEvent::eRightButton; + geckoEvent.mClickCount = [theEvent clickCount]; + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + mGeckoChild->DispatchInputEvent(&geckoEvent); + + NS_OBJC_END_TRY_ABORT_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.button = WidgetMouseEvent::eRightButton; + + // send event into Gecko by going directly to the + // the widget. + mGeckoChild->DispatchInputEvent(&geckoEvent); +} + +- (void)otherMouseDown:(NSEvent *)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + if ([self maybeRollup:theEvent] || + !ChildViewMouseTracker::WindowAcceptsEvent([self window], theEvent, self)) + return; + + if (!mGeckoChild) + return; + if (mTextInputHandler->OnHandleEvent(theEvent)) { + return; + } + + WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild, + WidgetMouseEvent::eReal); + [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent]; + geckoEvent.button = WidgetMouseEvent::eMiddleButton; + geckoEvent.mClickCount = [theEvent clickCount]; + + mGeckoChild->DispatchInputEvent(&geckoEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)otherMouseUp:(NSEvent *)theEvent +{ + if (!mGeckoChild) + return; + if (mTextInputHandler->OnHandleEvent(theEvent)) { + return; + } + + WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild, + WidgetMouseEvent::eReal); + [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent]; + geckoEvent.button = WidgetMouseEvent::eMiddleButton; + + 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]; + geckoEvent.button = WidgetMouseEvent::eMiddleButton; + + // 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 PanGestureInput::PanGestureType +PanGestureTypeForEvent(NSEvent* aEvent) +{ + switch (nsCocoaUtils::EventPhase(aEvent)) { + 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 (nsCocoaUtils::EventMomentumPhase(aEvent)) { + 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; + } +} + +static int32_t RoundUp(double aDouble) +{ + return aDouble < 0 ? static_cast<int32_t>(floor(aDouble)) : + static_cast<int32_t>(ceil(aDouble)); +} + +static int32_t +TakeLargestInt(gfx::Float* aFloat) +{ + int32_t result(*aFloat); // truncate towards zero + *aFloat -= result; + return result; +} + +static gfx::IntPoint +AccumulateIntegerDelta(NSEvent* aEvent) +{ + static gfx::Point sAccumulator(0.0f, 0.0f); + if (nsCocoaUtils::EventPhase(aEvent) == NSEventPhaseBegan) { + sAccumulator = gfx::Point(0.0f, 0.0f); + } + sAccumulator.x += [aEvent deltaX]; + sAccumulator.y += [aEvent deltaY]; + return gfx::IntPoint(TakeLargestInt(&sAccumulator.x), + TakeLargestInt(&sAccumulator.y)); +} + +static gfx::IntPoint +GetIntegerDeltaForEvent(NSEvent* aEvent) +{ + if (nsCocoaFeatures::OnSierraOrLater() && [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 AccumulateIntegerDelta(aEvent); + } + + // 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_ABORT_BLOCK; + + if (gfxPrefs::AsyncPanZoomSeparateEventThread() && [self apzctm]) { + // Disable main-thread scrolling completely when using APZ with the + // separate event thread. This is bug 1013412. + return; + } + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + ChildViewMouseTracker::MouseScrolled(theEvent); + + if ([self maybeRollup:theEvent]) { + return; + } + + if (!mGeckoChild) { + return; + } + + NSEventPhase phase = nsCocoaUtils::EventPhase(theEvent); + // 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 = nsCocoaUtils::HasPreciseScrollingDeltas(theEvent) && + Preferences::GetBool("mousewheel.enable_pixel_scrolling", true); + bool hasPhaseInformation = nsCocoaUtils::EventHasPhaseInformation(theEvent); + + gfx::IntPoint lineOrPageDelta = -GetIntegerDeltaForEvent(theEvent); + + Modifiers modifiers = nsCocoaUtils::ModifiersForEvent(theEvent); + + NSTimeInterval beforeNow = [[NSProcessInfo processInfo] systemUptime] - [theEvent timestamp]; + PRIntervalTime eventIntervalTime = PR_IntervalNow() - PR_MillisecondsToInterval(beforeNow * 1000); + TimeStamp eventTimeStamp = TimeStamp::Now() - TimeDuration::FromSeconds(beforeNow); + + ScreenPoint preciseDelta; + if (usePreciseDeltas) { + CGFloat pixelDeltaX = 0, pixelDeltaY = 0; + nsCocoaUtils::GetScrollingDeltas(theEvent, &pixelDeltaX, &pixelDeltaY); + double scale = geckoChildDeathGrip->BackingScaleFactor(); + preciseDelta = ScreenPoint(-pixelDeltaX * scale, -pixelDeltaY * scale); + } + + if (usePreciseDeltas && hasPhaseInformation) { + PanGestureInput panEvent(PanGestureTypeForEvent(theEvent), + eventIntervalTime, eventTimeStamp, + position, preciseDelta, modifiers); + panEvent.mLineOrPageDeltaX = lineOrPageDelta.x; + panEvent.mLineOrPageDeltaY = lineOrPageDelta.y; + + if (panEvent.mType == PanGestureInput::PANGESTURE_END) { + // Check if there's a momentum start event in the event queue, so that we + // can annotate this event. + NSEvent* nextWheelEvent = + [NSApp nextEventMatchingMask:NSScrollWheelMask + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:NO]; + if (nextWheelEvent && + PanGestureTypeForEvent(nextWheelEvent) == PanGestureInput::PANGESTURE_MOMENTUMSTART) { + panEvent.mFollowedByMomentum = true; + } + } + + bool canTriggerSwipe = [self shouldConsiderStartingSwipeFromEvent:theEvent]; + panEvent.mRequiresContentResponseIfCannotScrollHorizontallyInStartDirection = canTriggerSwipe; + geckoChildDeathGrip->DispatchAPZWheelInputEvent(panEvent, canTriggerSwipe); + } else if (usePreciseDeltas) { + // This is on 10.6 or old touchpads that don't have any phase information. + ScrollWheelInput wheelEvent(eventIntervalTime, eventTimeStamp, modifiers, + ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, + position, + preciseDelta.x, + preciseDelta.y, + false); + wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x; + wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y; + wheelEvent.mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(theEvent); + geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent, false); + } else { + ScrollWheelInput::ScrollMode scrollMode = ScrollWheelInput::SCROLLMODE_INSTANT; + if (gfxPrefs::SmoothScrollEnabled() && gfxPrefs::WheelSmoothScrollEnabled()) { + scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH; + } + ScrollWheelInput wheelEvent(eventIntervalTime, eventTimeStamp, modifiers, + scrollMode, + ScrollWheelInput::SCROLLDELTA_LINE, + position, + lineOrPageDelta.x, + lineOrPageDelta.y, + false); + wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x; + wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y; + geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent, false); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)handleAsyncScrollEvent:(CGEventRef)cgEvent ofType:(CGEventType)type +{ + IAPZCTreeManager* apzctm = [self apzctm]; + if (!apzctm) { + return; + } + + CGPoint loc = CGEventGetLocation(cgEvent); + loc.y = nsCocoaUtils::FlippedScreenY(loc.y); + NSPoint locationInWindow = + nsCocoaUtils::ConvertPointFromScreen([self window], NSPointFromCGPoint(loc)); + ScreenIntPoint location = ViewAs<ScreenPixel>( + [self convertWindowCoordinates:locationInWindow], + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + static NSTimeInterval sStartTime = [NSDate timeIntervalSinceReferenceDate]; + static TimeStamp sStartTimeStamp = TimeStamp::Now(); + + if (type == kCGEventScrollWheel) { + NSEvent* event = [NSEvent eventWithCGEvent:cgEvent]; + NSEventPhase phase = nsCocoaUtils::EventPhase(event); + NSEventPhase momentumPhase = nsCocoaUtils::EventMomentumPhase(event); + CGFloat pixelDeltaX = 0, pixelDeltaY = 0; + nsCocoaUtils::GetScrollingDeltas(event, &pixelDeltaX, &pixelDeltaY); + uint32_t eventTime = ([event timestamp] - sStartTime) * 1000; + TimeStamp eventTimeStamp = sStartTimeStamp + + TimeDuration::FromSeconds([event timestamp] - sStartTime); + NSPoint locationInWindowMoved = NSMakePoint( + locationInWindow.x + pixelDeltaX, + locationInWindow.y - pixelDeltaY); + ScreenIntPoint locationMoved = ViewAs<ScreenPixel>( + [self convertWindowCoordinates:locationInWindowMoved], + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + ScreenPoint delta = ScreenPoint(locationMoved - location); + ScrollableLayerGuid guid; + + // MayBegin and Cancelled are dispatched when the fingers start or stop + // touching the touchpad before any scrolling has occurred. These events + // can be used to control scrollbar visibility or interrupt scroll + // animations. They are only dispatched on 10.8 or later, and only by + // relatively modern devices. + if (phase == NSEventPhaseMayBegin) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_MAYSTART, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + return; + } + if (phase == NSEventPhaseCancelled) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_CANCELLED, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + return; + } + + // Legacy scroll events are dispatched by devices that do not have a + // concept of a scroll gesture, for example by USB mice with + // traditional mouse wheels. + // For these kinds of scrolls, we want to surround every single scroll + // event with a PANGESTURE_START and a PANGESTURE_END event. The APZC + // needs to know that the real scroll gesture can end abruptly after any + // one of these events. + bool isLegacyScroll = (phase == NSEventPhaseNone && + momentumPhase == NSEventPhaseNone && delta != ScreenPoint(0, 0)); + + if (phase == NSEventPhaseBegan || isLegacyScroll) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_START, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + if (momentumPhase == NSEventPhaseNone && delta != ScreenPoint(0, 0)) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_PAN, eventTime, + eventTimeStamp, location, delta, 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + if (phase == NSEventPhaseEnded || isLegacyScroll) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_END, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + + // Any device that can dispatch momentum events supports all three momentum phases. + if (momentumPhase == NSEventPhaseBegan) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_MOMENTUMSTART, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + if (momentumPhase == NSEventPhaseChanged && delta != ScreenPoint(0, 0)) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_MOMENTUMPAN, eventTime, + eventTimeStamp, location, delta, 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + if (momentumPhase == NSEventPhaseEnded) { + PanGestureInput panInput(PanGestureInput::PANGESTURE_MOMENTUMEND, eventTime, + eventTimeStamp, location, ScreenPoint(0, 0), 0); + apzctm->ReceiveInputEvent(panInput, &guid, nullptr); + } + } +} + +-(NSMenu*)menuForEvent:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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] == NSLeftMouseDown) { + [self mouseDown:theEvent]; + if (!mGeckoChild) + return nil; + } + + WidgetMouseEvent geckoEvent(true, eContextMenu, mGeckoChild, + WidgetMouseEvent::eReal); + [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent]; + geckoEvent.button = WidgetMouseEvent::eRightButton; + mGeckoChild->DispatchInputEvent(&geckoEvent); + if (!mGeckoChild) + return nil; + + [self maybeInitContextMenuTracking]; + + // Go up our view chain to fetch the correct menu to return. + return [self contextMenu]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSMenu*)contextMenu +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSView* superView = [self superview]; + if ([superView respondsToSelector:@selector(contextMenu)]) + return [(NSView<mozView>*)superView contextMenu]; + + return nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void) convertCocoaMouseWheelEvent:(NSEvent*)aMouseEvent + toGeckoEvent:(WidgetWheelEvent*)outWheelEvent +{ + [self convertCocoaMouseEvent:aMouseEvent toGeckoEvent:outWheelEvent]; + + bool usePreciseDeltas = nsCocoaUtils::HasPreciseScrollingDeltas(aMouseEvent) && + Preferences::GetBool("mousewheel.enable_pixel_scrolling", true); + + outWheelEvent->mDeltaMode = + usePreciseDeltas ? nsIDOMWheelEvent::DOM_DELTA_PIXEL + : nsIDOMWheelEvent::DOM_DELTA_LINE; + outWheelEvent->mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(aMouseEvent); +} + +- (void) convertCocoaMouseEvent:(NSEvent*)aMouseEvent + toGeckoEvent:(WidgetInputEvent*)outGeckoEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_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->buttons = 0; + NSUInteger mouseButtons = [NSEvent pressedMouseButtons]; + + if (mouseButtons & 0x01) { + mouseEvent->buttons |= WidgetMouseEvent::eLeftButtonFlag; + } + if (mouseButtons & 0x02) { + mouseEvent->buttons |= WidgetMouseEvent::eRightButtonFlag; + } + if (mouseButtons & 0x04) { + mouseEvent->buttons |= WidgetMouseEvent::eMiddleButtonFlag; + } + if (mouseButtons & 0x08) { + mouseEvent->buttons |= WidgetMouseEvent::e4thButtonFlag; + } + if (mouseButtons & 0x10) { + mouseEvent->buttons |= WidgetMouseEvent::e5thButtonFlag; + } + + switch ([aMouseEvent type]) { + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSLeftMouseDragged: + case NSRightMouseDown: + case NSRightMouseUp: + case NSRightMouseDragged: + case NSOtherMouseDown: + case NSOtherMouseUp: + case NSOtherMouseDragged: + if ([aMouseEvent subtype] == NSTabletPointEventSubtype) { + mouseEvent->pressure = [aMouseEvent pressure]; + MOZ_ASSERT(mouseEvent->pressure >= 0.0 && mouseEvent->pressure <= 1.0); + } + break; + + default: + // Don't check other NSEvents for pressure. + break; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (BOOL)shouldZoomOnDoubleClick +{ + if ([NSWindow respondsToSelector:@selector(_shouldZoomOnDoubleClick)]) { + return [NSWindow _shouldZoomOnDoubleClick]; + } + return nsCocoaFeatures::OnYosemiteOrLater(); +} + +- (BOOL)shouldMinimizeOnTitlebarDoubleClick +{ + NSString *MDAppleMiniaturizeOnDoubleClickKey = + @"AppleMiniaturizeOnDoubleClick"; + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + bool shouldMinimize = [[userDefaults + objectForKey:MDAppleMiniaturizeOnDoubleClickKey] boolValue]; + + return shouldMinimize; +} + +#pragma mark - +// NSTextInputClient implementation + +- (NSRange)markedRange +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0)); + return mTextInputHandler->MarkedRange(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSMakeRange(0, 0)); +} + +- (NSRange)selectedRange +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0)); + return mTextInputHandler->SelectedRange(); + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK_NIL; + + NS_ENSURE_TRUE(mTextInputHandler, [NSArray array]); + return mTextInputHandler->GetValidAttributesForMarkedText(); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +- (void)doCommandBySelector:(SEL)aSelector +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mGeckoChild || !mTextInputHandler) { + return; + } + + const char* sel = reinterpret_cast<const char*>(aSelector); + if (!mTextInputHandler->DoCommandBySelector(sel)) { + [super doCommandBySelector:aSelector]; + } + + NS_OBJC_END_TRY_ABORT_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_ABORT_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_ABORT_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_ABORT_BLOCK_RETURN; + + NS_ENSURE_TRUE(mTextInputHandler, [[self window] level]); + return mTextInputHandler->GetWindowLevel(); + + NS_OBJC_END_TRY_ABORT_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_ABORT_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()) { +#ifdef MOZ_CRASHREPORTER + 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] & (1 << 14)) != 0, + [NSApp isActive]]; + nsAutoCString additionalInfo([info UTF8String]); +#endif + if (mGeckoChild->GetInputContext().IsPasswordEditor() && + !TextInputHandler::IsSecureEventInputEnabled()) { + #define CRASH_MESSAGE "A password editor has focus, but not in secure input mode" +#ifdef MOZ_CRASHREPORTER + CrashReporter::AppendAppNotesToCrashReport(NS_LITERAL_CSTRING("\nBug 893973: ") + + NS_LITERAL_CSTRING(CRASH_MESSAGE)); + CrashReporter::AppendAppNotesToCrashReport(additionalInfo); +#endif + 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" +#ifdef MOZ_CRASHREPORTER + CrashReporter::AppendAppNotesToCrashReport(NS_LITERAL_CSTRING("\nBug 893973: ") + + NS_LITERAL_CSTRING(CRASH_MESSAGE)); + CrashReporter::AppendAppNotesToCrashReport(additionalInfo); +#endif + MOZ_CRASH(CRASH_MESSAGE); + #undef CRASH_MESSAGE + } + } +#endif // #if !defined(RELEASE_OR_BETA) || defined(DEBUG) + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + bool handled = false; + if (mGeckoChild && mTextInputHandler) { + handled = mTextInputHandler->HandleKeyDownEvent(theEvent); + } + + // We always allow keyboard events to propagate to keyDown: but if they are not + // handled we give special Application menu items a chance to act. + if (!handled && sApplicationMenu) { + [sApplicationMenu performKeyEquivalent:theEvent]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)keyUp:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NS_ENSURE_TRUE(mGeckoChild, ); + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + mTextInputHandler->HandleKeyUpEvent(theEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)insertNewline:(id)sender +{ + if (mTextInputHandler) { + NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"\n"]; + mTextInputHandler->InsertText(attrStr); + [attrStr release]; + } +} + +- (void)flagsChanged:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NS_ENSURE_TRUE(mGeckoChild, ); + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + mTextInputHandler->HandleFlagsChanged(theEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (BOOL) isFirstResponder +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSResponder* resp = [[self window] firstResponder]; + return (resp == (NSResponder*)self); + + NS_OBJC_END_TRY_ABORT_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) != 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_ABORT_BLOCK_RETURN; + + return [super becomeFirstResponder]; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(YES); +} + +- (void)viewsWindowDidBecomeKey +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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_ABORT_BLOCK; + + [self removeFromSuperview]; + [self release]; + + NS_OBJC_END_TRY_ABORT_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); +} + +- (IAPZCTreeManager*)apzctm +{ + return mGeckoChild ? mGeckoChild->APZCTM() : nullptr; +} + +// 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_ABORT_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 + mDragService->FireDragEventAtSource(eDrag); + 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<nsIDOMNode> sourceNode; + dragSession->GetSourceNode(getter_AddRefs(sourceNode)); + if (!sourceNode) { + mDragService->EndDragSession(false); + } + 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 & NSAlternateKeyMask) { + if (modifierFlags & NSCommandKeyMask) + 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<nsIDOMNode> 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). + mDragService->EndDragSession(false); + } + } + default: + break; + } + } + + return NSDragOperationGeneric; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSDragOperationNone); +} + +- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK_RETURN(NSDragOperationNone); +} + +- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender +{ + MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingUpdated: entered\n")); + + return [self doDragAction:eDragOver sender:sender]; +} + +- (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); +} + +- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender +{ + nsAutoRetainCocoaObject kungFuDeathGrip(self); + BOOL handled = [self doDragAction:eDrop sender:sender] != NSDragOperationNone; + NS_IF_RELEASE(mDragService); + return handled; +} + +// NSDraggingSource +- (void)draggedImage:(NSImage *)anImage movedTo:(NSPoint)aPoint +{ + // 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) { + NSPoint pnt = [NSEvent mouseLocation]; + FlipCocoaScreenCoordinate(pnt); + + LayoutDeviceIntPoint devPoint = mGeckoChild->CocoaPointsToDevPixels(pnt); + dragService->DragMoved(devPoint.x, devPoint.y); + } +} + +// NSDraggingSource +- (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + gDraggedTransferables = nullptr; + + NSEvent *currentEvent = [NSApp currentEvent]; + gUserCancelledDrag = ([currentEvent type] == NSKeyDown && + [currentEvent keyCode] == kVK_Escape); + + if (!mDragService) { + CallGetService(kDragServiceContractID, &mDragService); + NS_ASSERTION(mDragService, "Couldn't get a drag service - big problem!"); + } + + if (mDragService) { + // set the dragend point from the current mouse location + nsDragService* dragService = static_cast<nsDragService *>(mDragService); + NSPoint pnt = [NSEvent mouseLocation]; + FlipCocoaScreenCoordinate(pnt); + dragService->SetDragEndPoint(gfx::IntPoint::Round(pnt.x, pnt.y)); + + // XXX: dropEffect should be updated per |operation|. + // As things stand though, |operation| 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 (operation == NSDragOperationNone) { + nsCOMPtr<nsIDOMDataTransfer> dataTransfer; + dragService->GetDataTransfer(getter_AddRefs(dataTransfer)); + if (dataTransfer) + dataTransfer->SetDropEffectInt(nsIDragService::DRAGDROP_ACTION_NONE); + } + + mDragService->EndDragSession(true); + NS_RELEASE(mDragService); + } + + [globalDragPboard release]; + globalDragPboard = nil; + [gLastDragMouseDownEvent release]; + gLastDragMouseDownEvent = nil; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// NSDraggingSource +// this is just implemented so we comply with the NSDraggingSource informal protocol +- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return UINT_MAX; +} + +// This method is a callback typically invoked in response to a drag ending on the desktop +// or a Findow folder window; the argument passed is a path to the drop location, to be used +// in constructing a complete pathname for the file(s) we want to create as a result of +// the drag. +- (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDestination +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + nsresult rv; + + MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView namesOfPromisedFilesDroppedAtDestination: entering callback for promised files\n")); + + nsCOMPtr<nsIFile> targFile; + NS_NewLocalFile(EmptyString(), true, getter_AddRefs(targFile)); + nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(targFile); + if (!macLocalFile) { + NS_ERROR("No Mac local file"); + return nil; + } + + if (!NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)dropDestination))) { + NS_ERROR("failed InitWithCFURL"); + return nil; + } + + if (!gDraggedTransferables) + return nil; + + uint32_t transferableCount; + rv = gDraggedTransferables->GetLength(&transferableCount); + if (NS_FAILED(rv)) + return nil; + + for (uint32_t i = 0; i < transferableCount; i++) { + nsCOMPtr<nsITransferable> item = do_QueryElementAt(gDraggedTransferables, i); + if (!item) { + NS_ERROR("no transferable"); + return nil; + } + + item->SetTransferData(kFilePromiseDirectoryMime, macLocalFile, sizeof(nsIFile*)); + + // now request the kFilePromiseMime data, which will invoke the data provider + // If successful, the returned data is a reference to the resulting file. + nsCOMPtr<nsISupports> fileDataPrimitive; + uint32_t dataSize = 0; + item->GetTransferData(kFilePromiseMime, getter_AddRefs(fileDataPrimitive), &dataSize); + } + + NSPasteboard* generalPboard = [NSPasteboard pasteboardWithName:NSDragPboard]; + NSData* data = [generalPboard dataForType:@"application/x-moz-file-promise-dest-filename"]; + NSString* name = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray* rslt = [NSArray arrayWithObject:name]; + + [name release]; + + return rslt; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +#pragma mark - + +// Support for the "Services" menu. We currently only support sending strings +// and HTML to system services. + +- (id)validRequestorForSendType:(NSString *)sendType + returnType:(NSString *)returnType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // 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. + +#define IsSupportedType(typeStr) ([typeStr isEqual:NSStringPboardType] || [typeStr isEqual:NSHTMLPboardType]) + + id result = nil; + + if ((!sendType || IsSupportedType(sendType)) && + (!returnType || IsSupportedType(returnType))) { + 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; + } + } + } + +#undef IsSupportedType + + // 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_ABORT_BLOCK_NIL; +} + +- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard + types:(NSArray *)types +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + nsAutoRetainCocoaObject kungFuDeathGrip(self); + + // Make sure that the service will accept strings or HTML. + if ([types containsObject:NSStringPboardType] == NO && + [types containsObject:NSHTMLPboardType] == NO) + 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 == NSStringPboardType || + currentKey == kCorePboardType_url || + currentKey == kCorePboardType_urld || + currentKey == kCorePboardType_urln) { + [pboard setString:currentValue forType:currentKey]; + } else if (currentKey == NSHTMLPboardType) { + [pboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue)) forType:currentKey]; + } else if (currentKey == NSTIFFPboardType) { + [pboard setData:currentValue forType:currentKey]; + } else if (currentKey == NSFilesPromisePboardType) { + [pboard setPropertyList:currentValue forType:currentKey]; + } + } + + return YES; + + NS_OBJC_END_TRY_ABORT_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; +} + +NS_IMETHODIMP +nsChildView::GetSelectionAsPlaintext(nsAString& aResult) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +#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::Accessible> 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]; +} + +#ifdef DEBUG +- (void)printHierarchy +{ + [[self accessible] printHierarchy]; +} +#endif + +#pragma mark - + +// general + +- (BOOL)accessibilityIsIgnored +{ + if (!mozilla::a11y::ShouldA11yBeEnabled()) + return [super accessibilityIsIgnored]; + + return [[self accessible] accessibilityIsIgnored]; +} + +- (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_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_NIL; +} + +#endif /* ACCESSIBILITY */ + +@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; + ReEvaluateMouseEnterState(aEvent); + } +} + +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::eChild : WidgetMouseEvent::eTopLevel; + [oldView sendMouseEnterOrExitEvent:aEvent + enter:NO + exitFrom:exitFrom]; + // After the cursor exits the window set it to a visible regular arrow cursor. + if (exitFrom == WidgetMouseEvent::eTopLevel) { + [[nsCursorManager sharedInstance] setCursor: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] == NSRightMouseDown) + 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] == NSOtherMouseDown || + (([aEvent modifierFlags] & NSCommandKeyMask) != 0 && + [aEvent type] != NSMouseMoved)) + 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 - + +@interface EventThreadRunner(Private) +- (void)runEventThread; +- (void)shutdownAndReleaseCalledOnEventThread; +- (void)shutdownAndReleaseCalledOnAnyThread; +- (void)handleEvent:(CGEventRef)cgEvent type:(CGEventType)type; +@end + +static EventThreadRunner* sEventThreadRunner = nil; + +@implementation EventThreadRunner + ++ (void)start +{ + sEventThreadRunner = [[EventThreadRunner alloc] init]; +} + ++ (void)stop +{ + if (sEventThreadRunner) { + [sEventThreadRunner shutdownAndReleaseCalledOnAnyThread]; + sEventThreadRunner = nil; + } +} + +- (id)init +{ + if ((self = [super init])) { + mThread = nil; + [NSThread detachNewThreadSelector:@selector(runEventThread) + toTarget:self + withObject:nil]; + } + return self; +} + +static CGEventRef +HandleEvent(CGEventTapProxy aProxy, CGEventType aType, + CGEventRef aEvent, void* aClosure) +{ + [(EventThreadRunner*)aClosure handleEvent:aEvent type:aType]; + return aEvent; +} + +- (void)runEventThread +{ + char aLocal; + profiler_register_thread("APZC Event Thread", &aLocal); + PR_SetCurrentThreadName("APZC Event Thread"); + + mThread = [NSThread currentThread]; + ProcessSerialNumber currentProcess; + GetCurrentProcess(¤tProcess); + CFMachPortRef eventPort = + CGEventTapCreateForPSN(¤tProcess, + kCGHeadInsertEventTap, + kCGEventTapOptionListenOnly, + CGEventMaskBit(kCGEventScrollWheel), + HandleEvent, + self); + CFRunLoopSourceRef eventPortSource = + CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, eventPort, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), eventPortSource, kCFRunLoopCommonModes); + CFRunLoopRun(); + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventPortSource, kCFRunLoopCommonModes); + CFRelease(eventPortSource); + CFRelease(eventPort); + [self release]; +} + +- (void)shutdownAndReleaseCalledOnEventThread +{ + CFRunLoopStop(CFRunLoopGetCurrent()); +} + +- (void)shutdownAndReleaseCalledOnAnyThread +{ + [self performSelector:@selector(shutdownAndReleaseCalledOnEventThread) onThread:mThread withObject:nil waitUntilDone:NO]; +} + +static const CGEventField kCGWindowNumberField = (const CGEventField) 51; + +// Called on scroll thread +- (void)handleEvent:(CGEventRef)cgEvent type:(CGEventType)type +{ + if (type != kCGEventScrollWheel) { + return; + } + + int windowNumber = CGEventGetIntegerValueField(cgEvent, kCGWindowNumberField); + NSWindow* window = [NSApp windowWithWindowNumber:windowNumber]; + if (!window || ![window isKindOfClass:[BaseWindow class]]) { + return; + } + + ChildView* childView = [(BaseWindow*)window mainChildView]; + [childView handleAsyncScrollEvent:cgEvent ofType:type]; +} + +@end + +@interface NSView (MethodSwizzling) +- (BOOL)nsChildView_NSView_mouseDownCanMoveWindow; +@end + +@implementation NSView (MethodSwizzling) + +// All top-level browser windows belong to the ToolbarWindow class and have +// NSTexturedBackgroundWindowMask turned on in their "style" (see particularly +// [ToolbarWindow initWithContentRect:...] in nsCocoaWindow.mm). This style +// normally means the window "may be moved by clicking and dragging anywhere +// in the window background", but we've suppressed this by giving the +// ChildView class a mouseDownCanMoveWindow method that always returns NO. +// Normally a ToolbarWindow's contentView (not a ChildView) returns YES when +// NSTexturedBackgroundWindowMask is turned on. But normally this makes no +// difference. However, under some (probably very unusual) circumstances +// (and only on Leopard) it *does* make a difference -- for example it +// triggers bmo bugs 431902 and 476393. So here we make sure that a +// ToolbarWindow's contentView always returns NO from the +// mouseDownCanMoveWindow method. +- (BOOL)nsChildView_NSView_mouseDownCanMoveWindow +{ + NSWindow *ourWindow = [self window]; + NSView *contentView = [ourWindow contentView]; + if ([ourWindow isKindOfClass:[ToolbarWindow class]] && (self == contentView)) + return [ourWindow isMovableByWindowBackground]; + return [self nsChildView_NSView_mouseDownCanMoveWindow]; +} + +@end + +#ifdef __LP64__ +// When using blocks, at least on OS X 10.7, the OS sometimes calls +// +[NSEvent removeMonitor:] more than once on a single event monitor, which +// causes crashes. See bug 678607. We hook these methods to work around +// the problem. +@interface NSEvent (MethodSwizzling) ++ (id)nsChildView_NSEvent_addLocalMonitorForEventsMatchingMask:(unsigned long long)mask handler:(id)block; ++ (void)nsChildView_NSEvent_removeMonitor:(id)eventMonitor; +@end + +// This is a local copy of the AppKit frameworks sEventObservers hashtable. +// It only stores "local monitors". We use it to ensure that +[NSEvent +// removeMonitor:] is never called more than once on the same local monitor. +static NSHashTable *sLocalEventObservers = nil; + +@implementation NSEvent (MethodSwizzling) + ++ (id)nsChildView_NSEvent_addLocalMonitorForEventsMatchingMask:(unsigned long long)mask handler:(id)block +{ + if (!sLocalEventObservers) { + sLocalEventObservers = [[NSHashTable hashTableWithOptions: + NSHashTableStrongMemory | NSHashTableObjectPointerPersonality] retain]; + } + id retval = + [self nsChildView_NSEvent_addLocalMonitorForEventsMatchingMask:mask handler:block]; + if (sLocalEventObservers && retval && ![sLocalEventObservers containsObject:retval]) { + [sLocalEventObservers addObject:retval]; + } + return retval; +} + ++ (void)nsChildView_NSEvent_removeMonitor:(id)eventMonitor +{ + if (sLocalEventObservers && [eventMonitor isKindOfClass: ::NSClassFromString(@"_NSLocalEventObserver")]) { + if (![sLocalEventObservers containsObject:eventMonitor]) { + return; + } + [sLocalEventObservers removeObject:eventMonitor]; + } + [self nsChildView_NSEvent_removeMonitor:eventMonitor]; +} + +@end +#endif // #ifdef __LP64__ diff --git a/widget/cocoa/nsClipboard.h b/widget/cocoa/nsClipboard.h new file mode 100644 index 000000000..45871efe1 --- /dev/null +++ b/widget/cocoa/nsClipboard.h @@ -0,0 +1,55 @@ +/* -*- 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 "nsIClipboard.h" +#include "nsXPIDLString.h" +#include "mozilla/StaticPtr.h" + +#import <Cocoa/Cocoa.h> + +class nsITransferable; + +class nsClipboard : public nsIClipboard +{ + +public: + nsClipboard(); + + NS_DECL_ISUPPORTS + NS_DECL_NSICLIPBOARD + + // 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); + static bool IsStringType(const nsCString& aMIMEType, NSString** aPasteboardType); + static NSString* WrapHtmlForSystemPasteboard(NSString* aString); + static nsresult TransferableFromPasteboard(nsITransferable *aTransferable, NSPasteboard *pboard); + +protected: + + // impelement the native clipboard behavior + NS_IMETHOD SetNativeClipboardData(int32_t aWhichClipboard); + NS_IMETHOD GetNativeClipboardData(nsITransferable * aTransferable, int32_t aWhichClipboard); + void ClearSelectionCache(); + void SetSelectionCache(nsITransferable* aTransferable); + +private: + virtual ~nsClipboard(); + int32_t mCachedClipboard; + int32_t mChangeCount; // Set to the native change count after any modification of the clipboard. + + bool mIgnoreEmptyNotification; + nsCOMPtr<nsIClipboardOwner> mClipboardOwner; + nsCOMPtr<nsITransferable> mTransferable; +}; + +#endif // nsClipboard_h_ diff --git a/widget/cocoa/nsClipboard.mm b/widget/cocoa/nsClipboard.mm new file mode 100644 index 000000000..4146f1785 --- /dev/null +++ b/widget/cocoa/nsClipboard.mm @@ -0,0 +1,775 @@ +/* -*- 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 "mozilla/Unused.h" + +#include "gfxPlatform.h" +#include "nsArrayUtils.h" +#include "nsCOMPtr.h" +#include "nsClipboard.h" +#include "nsString.h" +#include "nsISupportsPrimitives.h" +#include "nsXPIDLString.h" +#include "nsPrimitiveHelpers.h" +#include "nsMemory.h" +#include "nsIFile.h" +#include "nsStringStream.h" +#include "nsDragService.h" +#include "nsEscape.h" +#include "nsPrintfCString.h" +#include "nsObjCExceptions.h" +#include "imgIContainer.h" +#include "nsCocoaUtils.h" + +using mozilla::gfx::DataSourceSurface; +using mozilla::gfx::SourceSurface; +using mozilla::LogLevel; + +// Screenshots use the (undocumented) png pasteboard type. +#define IMAGE_PASTEBOARD_TYPES NSTIFFPboardType, @"Apple PNG pasteboard type", nil + +extern PRLogModuleInfo* sCocoaLog; + +extern void EnsureLogInitialized(); + +mozilla::StaticRefPtr<nsITransferable> nsClipboard::sSelectionCache; + +nsClipboard::nsClipboard() + : mCachedClipboard(-1) + , mChangeCount(0) + , mIgnoreEmptyNotification(false) +{ + EnsureLogInitialized(); +} + +nsClipboard::~nsClipboard() +{ + EmptyClipboard(kGlobalClipboard); + EmptyClipboard(kFindClipboard); + ClearSelectionCache(); +} + +NS_IMPL_ISUPPORTS(nsClipboard, nsIClipboard) + +// 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_ABORT_BLOCK_NSRESULT; + + if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard) || !mTransferable) + return NS_ERROR_FAILURE; + + mIgnoreEmptyNotification = true; + + 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]; + [cocoaPasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] 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 == NSStringPboardType) + [cocoaPasteboard setString:currentValue forType:currentKey]; + } else { + if (currentKey == NSStringPboardType || + currentKey == kCorePboardType_url || + currentKey == kCorePboardType_urld || + currentKey == kCorePboardType_urln) { + [cocoaPasteboard setString:currentValue forType:currentKey]; + } else if (currentKey == NSHTMLPboardType) { + [cocoaPasteboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue)) + forType:currentKey]; + } else { + [cocoaPasteboard setData:currentValue forType:currentKey]; + } + } + } + + mCachedClipboard = aWhichClipboard; + mChangeCount = [cocoaPasteboard changeCount]; + + mIgnoreEmptyNotification = false; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult +nsClipboard::TransferableFromPasteboard(nsITransferable *aTransferable, NSPasteboard *cocoaPasteboard) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // get flavor list that includes all acceptable flavors (including ones obtained through conversion) + nsCOMPtr<nsIArray> flavorList; + nsresult rv = aTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavorList)); + if (NS_FAILED(rv)) + return NS_ERROR_FAILURE; + + uint32_t flavorCount; + flavorList->GetLength(&flavorCount); + + for (uint32_t i = 0; i < flavorCount; i++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, i); + if (!currentFlavor) + continue; + + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); // i has a flavr + + // 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:NSRTFPboardType]) { + 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]; + + // 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, genericDataWrapper, dataLength); + free(clipboardDataPtr); + break; + } + else if (flavorStr.EqualsLiteral(kCustomTypesMime)) { + NSString* type = [cocoaPasteboard availableTypeFromArray:[NSArray arrayWithObject:kCustomTypesPboardType]]; + 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]; + + nsCOMPtr<nsISupports> genericDataWrapper; + nsPrimitiveHelpers::CreatePrimitiveForData(flavorStr, clipboardDataPtr, dataLength, + getter_AddRefs(genericDataWrapper)); + + aTransferable->SetTransferData(flavorStr, genericDataWrapper, dataLength); + 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:IMAGE_PASTEBOARD_TYPES]]; + 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 == NSTIFFPboardType ? @"public.tiff" : @"public.png"), + kCGImageSourceTypeIdentifierHint, nil]; + + CGImageSourceRef 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), (const char*)[encodedData bytes], + [encodedData length], NS_ASSIGNMENT_COPY); + + aTransferable->SetTransferData(flavorStr, byteStream, sizeof(nsIInputStream*)); + } + + if (dest) + CFRelease(dest); + if (source) + CFRelease(source); + + if (successfullyConverted) + break; + else + continue; + } + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable, int32_t aWhichClipboard) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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) + nsCOMPtr<nsIArray> flavorList; + nsresult rv = aTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavorList)); + if (NS_FAILED(rv)) + return NS_ERROR_FAILURE; + + uint32_t flavorCount; + flavorList->GetLength(&flavorCount); + + // 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 < flavorCount; i++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, i); + if (!currentFlavor) + continue; + + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); + + nsCOMPtr<nsISupports> dataSupports; + uint32_t dataSize = 0; + rv = mTransferable->GetTransferData(flavorStr, getter_AddRefs(dataSupports), &dataSize); + if (NS_SUCCEEDED(rv)) { + aTransferable->SetTransferData(flavorStr, dataSupports, dataSize); + return NS_OK; // maybe try to fill in more types? Is there a point? + } + } + } + } else { + EmptyClipboard(aWhichClipboard); + } + + // 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_ABORT_BLOCK_NSRESULT; +} + +// returns true if we have *any* of the passed in flavors available for pasting +NS_IMETHODIMP +nsClipboard::HasDataMatchingFlavors(const char** aFlavorList, uint32_t aLength, + int32_t aWhichClipboard, bool* outResult) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + *outResult = false; + + if ((aWhichClipboard != kGlobalClipboard) || !aFlavorList) + return NS_OK; + + // first see if we have data for this in our cached transferable + if (mTransferable) { + nsCOMPtr<nsIArray> transferableFlavorList; + nsresult rv = mTransferable->FlavorsTransferableCanImport(getter_AddRefs(transferableFlavorList)); + if (NS_SUCCEEDED(rv)) { + uint32_t transferableFlavorCount; + transferableFlavorList->GetLength(&transferableFlavorCount); + for (uint32_t j = 0; j < transferableFlavorCount; j++) { + nsCOMPtr<nsISupportsCString> currentTransferableFlavor = + do_QueryElementAt(transferableFlavorList, j); + if (!currentTransferableFlavor) + continue; + nsXPIDLCString transferableFlavorStr; + currentTransferableFlavor->ToString(getter_Copies(transferableFlavorStr)); + + for (uint32_t k = 0; k < aLength; k++) { + if (transferableFlavorStr.Equals(aFlavorList[k])) { + *outResult = true; + return NS_OK; + } + } + } + } + } + + NSPasteboard* generalPBoard = [NSPasteboard generalPasteboard]; + + for (uint32_t i = 0; i < aLength; i++) { + nsDependentCString mimeType(aFlavorList[i]); + NSString *pboardType = nil; + + if (nsClipboard::IsStringType(mimeType, &pboardType)) { + NSString* availableType = [generalPBoard availableTypeFromArray:[NSArray arrayWithObject:pboardType]]; + if (availableType && [availableType isEqualToString:pboardType]) { + *outResult = true; + break; + } + } else if (!strcmp(aFlavorList[i], kCustomTypesMime)) { + NSString* availableType = [generalPBoard availableTypeFromArray:[NSArray arrayWithObject:kCustomTypesPboardType]]; + if (availableType) { + *outResult = true; + break; + } + } else if (!strcmp(aFlavorList[i], kJPEGImageMime) || + !strcmp(aFlavorList[i], kJPGImageMime) || + !strcmp(aFlavorList[i], kPNGImageMime) || + !strcmp(aFlavorList[i], kGIFImageMime)) { + NSString* availableType = [generalPBoard availableTypeFromArray: + [NSArray arrayWithObjects:IMAGE_PASTEBOARD_TYPES]]; + if (availableType) { + *outResult = true; + break; + } + } + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsClipboard::SupportsFindClipboard(bool *_retval) +{ + NS_ENSURE_ARG_POINTER(_retval); + *_retval = true; + return NS_OK; +} + +// 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_ABORT_BLOCK_NIL; + + if (!aTransferable) + return nil; + + NSMutableDictionary* pasteboardOutputDict = [NSMutableDictionary dictionary]; + + nsCOMPtr<nsIArray> flavorList; + nsresult rv = aTransferable->FlavorsTransferableCanExport(getter_AddRefs(flavorList)); + if (NS_FAILED(rv)) + return nil; + + uint32_t flavorCount; + flavorList->GetLength(&flavorCount); + for (uint32_t i = 0; i < flavorCount; i++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, i); + if (!currentFlavor) + continue; + + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); + + MOZ_LOG(sCocoaLog, LogLevel::Info, ("writing out clipboard data of type %s (%d)\n", flavorStr.get(), i)); + + NSString *pboardType = nil; + + if (nsClipboard::IsStringType(flavorStr, &pboardType)) { + void* data = nullptr; + uint32_t dataSize = 0; + nsCOMPtr<nsISupports> genericDataWrapper; + rv = aTransferable->GetTransferData(flavorStr, getter_AddRefs(genericDataWrapper), &dataSize); + nsPrimitiveHelpers::CreateDataFromPrimitive(flavorStr, genericDataWrapper, &data, dataSize); + + NSString* nativeString; + if (data) + nativeString = [NSString stringWithCharacters:(const unichar*)data length:(dataSize / sizeof(char16_t))]; + else + nativeString = [NSString string]; + + // be nice to Carbon apps, normalize the receiver's contents using Form C. + nativeString = [nativeString precomposedStringWithCanonicalMapping]; + + [pasteboardOutputDict setObject:nativeString forKey:pboardType]; + + free(data); + } + else if (flavorStr.EqualsLiteral(kCustomTypesMime)) { + void* data = nullptr; + uint32_t dataSize = 0; + nsCOMPtr<nsISupports> genericDataWrapper; + rv = aTransferable->GetTransferData(flavorStr, getter_AddRefs(genericDataWrapper), &dataSize); + nsPrimitiveHelpers::CreateDataFromPrimitive(flavorStr, genericDataWrapper, &data, dataSize); + + if (data) { + NSData* nativeData = [NSData dataWithBytes:data length:dataSize]; + + [pasteboardOutputDict setObject:nativeData forKey:kCustomTypesPboardType]; + free(data); + } + } + else if (flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime) || + flavorStr.EqualsLiteral(kNativeImageMime)) { + uint32_t dataSize = 0; + nsCOMPtr<nsISupports> transferSupports; + aTransferable->GetTransferData(flavorStr, getter_AddRefs(transferSupports), &dataSize); + nsCOMPtr<nsISupportsInterfacePointer> ptrPrimitive(do_QueryInterface(transferSupports)); + if (!ptrPrimitive) + continue; + + nsCOMPtr<nsISupports> primitiveData; + ptrPrimitive->GetData(getter_AddRefs(primitiveData)); + + nsCOMPtr<imgIContainer> image(do_QueryInterface(primitiveData)); + if (!image) { + NS_WARNING("Image isn't an imgIContainer in transferable"); + continue; + } + + RefPtr<SourceSurface> surface = + image->GetFrame(imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_SYNC_DECODE); + 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) { + if (tiffData) + CFRelease(tiffData); + continue; + } + + [pasteboardOutputDict setObject:(NSMutableData*)tiffData forKey:NSTIFFPboardType]; + if (tiffData) + CFRelease(tiffData); + } + else if (flavorStr.EqualsLiteral(kFileMime)) { + uint32_t len = 0; + nsCOMPtr<nsISupports> genericFile; + rv = aTransferable->GetTransferData(flavorStr, getter_AddRefs(genericFile), &len); + if (NS_FAILED(rv)) { + continue; + } + + nsCOMPtr<nsIFile> file(do_QueryInterface(genericFile)); + if (!file) { + nsCOMPtr<nsISupportsInterfacePointer> ptr(do_QueryInterface(genericFile)); + + if (ptr) { + ptr->GetData(getter_AddRefs(genericFile)); + file = do_QueryInterface(genericFile); + } + } + + if (!file) { + continue; + } + + nsAutoString fileURI; + rv = file->GetPath(fileURI); + if (NS_FAILED(rv)) { + continue; + } + + NSString* str = nsCocoaUtils::ToNSString(fileURI); + NSArray* fileList = [NSArray arrayWithObjects:str, nil]; + [pasteboardOutputDict setObject:fileList forKey:NSFilenamesPboardType]; + } + else if (flavorStr.EqualsLiteral(kFilePromiseMime)) { + [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""] forKey:NSFilesPromisePboardType]; + } + else if (flavorStr.EqualsLiteral(kURLMime)) { + uint32_t len = 0; + nsCOMPtr<nsISupports> genericURL; + rv = aTransferable->GetTransferData(flavorStr, getter_AddRefs(genericURL), &len); + nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL)); + + nsAutoString url; + urlObject->GetData(url); + + // A newline embedded in the URL means that the form is actually URL + title. + int32_t newlinePos = url.FindChar(char16_t('\n')); + if (newlinePos >= 0) { + url.Truncate(newlinePos); + + nsAutoString urlTitle; + urlObject->GetData(urlTitle); + urlTitle.Mid(urlTitle, newlinePos + 1, len - (newlinePos + 1)); + + NSString *nativeTitle = [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(urlTitle.get()) + length:urlTitle.Length()]; + // be nice to Carbon apps, normalize the receiver's contents using Form C. + [pasteboardOutputDict setObject:[nativeTitle precomposedStringWithCanonicalMapping] forKey:kCorePboardType_urln]; + // Also put the title out as 'urld', since some recipients will look for that. + [pasteboardOutputDict setObject:[nativeTitle precomposedStringWithCanonicalMapping] forKey:kCorePboardType_urld]; + [nativeTitle release]; + } + + // 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); + + // printf("Escaped url is %s, length %d\n", escData.get(), escData.Length()); + + NSString *nativeURL = [NSString stringWithUTF8String:escData.get()]; + [pasteboardOutputDict setObject:nativeURL forKey:kCorePboardType_url]; + } + // 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_ABORT_BLOCK_NIL; +} + +bool nsClipboard::IsStringType(const nsCString& aMIMEType, NSString** aPasteboardType) +{ + if (aMIMEType.EqualsLiteral(kUnicodeMime)) { + *aPasteboardType = NSStringPboardType; + return true; + } else if (aMIMEType.EqualsLiteral(kRTFMime)) { + *aPasteboardType = NSRTFPboardType; + return true; + } else if (aMIMEType.EqualsLiteral(kHTMLMime)) { + *aPasteboardType = NSHTMLPboardType; + return true; + } else { + return false; + } +} + +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; + } + + if (aTransferable == mTransferable && anOwner == mClipboardOwner) { + return NS_OK; + } + bool selectClipPresent; + SupportsSelectionClipboard(&selectClipPresent); + bool findClipPresent; + SupportsFindClipboard(&findClipPresent); + if (!selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard) { + return NS_ERROR_FAILURE; + } + + EmptyClipboard(aWhichClipboard); + + mClipboardOwner = anOwner; + mTransferable = aTransferable; + + nsresult rv = NS_ERROR_FAILURE; + if (mTransferable) { + rv = SetNativeClipboardData(aWhichClipboard); + } + + return rv; +} + +/** + * Gets the transferable object + * + */ +NS_IMETHODIMP +nsClipboard::GetData(nsITransferable* aTransferable, int32_t aWhichClipboard) +{ + NS_ASSERTION (aTransferable, "clipboard given a null transferable"); + + bool selectClipPresent; + SupportsSelectionClipboard(&selectClipPresent); + bool findClipPresent; + SupportsFindClipboard(&findClipPresent); + if (!selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard) + return NS_ERROR_FAILURE; + + if (aTransferable) { + return GetNativeClipboardData(aTransferable, aWhichClipboard); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsClipboard::EmptyClipboard(int32_t aWhichClipboard) +{ + if (aWhichClipboard == kSelectionCache) { + ClearSelectionCache(); + return NS_OK; + } + + bool selectClipPresent; + SupportsSelectionClipboard(&selectClipPresent); + bool findClipPresent; + SupportsFindClipboard(&findClipPresent); + if (!selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard) { + return NS_ERROR_FAILURE; + } + + if (mIgnoreEmptyNotification) { + return NS_OK; + } + + if (mClipboardOwner) { + mClipboardOwner->LosingOwnership(mTransferable); + mClipboardOwner = nullptr; + } + + mTransferable = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsClipboard::SupportsSelectionClipboard(bool* _retval) +{ + *_retval = false; // we don't support the selection clipboard by default. + return NS_OK; +} diff --git a/widget/cocoa/nsCocoaDebugUtils.h b/widget/cocoa/nsCocoaDebugUtils.h new file mode 100644 index 000000000..814f06087 --- /dev/null +++ b/widget/cocoa/nsCocoaDebugUtils.h @@ -0,0 +1,136 @@ +/* -*- 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 nsCocoaDebugUtils_h_ +#define nsCocoaDebugUtils_h_ + +#include <CoreServices/CoreServices.h> + +// Definitions and declarations of stuff used by us from the CoreSymbolication +// framework. This is an undocumented, private framework available on OS X +// 10.6 and up. It's used by Apple utilities like dtrace, atos, ReportCrash +// and crashreporterd. + +typedef struct _CSTypeRef { + unsigned long type; + void* contents; +} CSTypeRef; + +typedef CSTypeRef CSSymbolicatorRef; +typedef CSTypeRef CSSymbolOwnerRef; +typedef CSTypeRef CSSymbolRef; +typedef CSTypeRef CSSourceInfoRef; + +typedef struct _CSRange { + unsigned long long location; + unsigned long long length; +} CSRange; + +typedef unsigned long long CSArchitecture; + +#define kCSNow LONG_MAX + +extern "C" { + +CSSymbolicatorRef +CSSymbolicatorCreateWithPid(pid_t pid); + +CSSymbolicatorRef +CSSymbolicatorCreateWithPidFlagsAndNotification(pid_t pid, + uint32_t flags, + uint32_t notification); + +CSArchitecture +CSSymbolicatorGetArchitecture(CSSymbolicatorRef symbolicator); + +CSSymbolOwnerRef +CSSymbolicatorGetSymbolOwnerWithAddressAtTime(CSSymbolicatorRef symbolicator, + unsigned long long address, + long time); + +const char* +CSSymbolOwnerGetName(CSSymbolOwnerRef owner); + +unsigned long long +CSSymbolOwnerGetBaseAddress(CSSymbolOwnerRef owner); + +CSSymbolRef +CSSymbolOwnerGetSymbolWithAddress(CSSymbolOwnerRef owner, + unsigned long long address); + +CSSourceInfoRef +CSSymbolOwnerGetSourceInfoWithAddress(CSSymbolOwnerRef owner, + unsigned long long address); + +const char* +CSSymbolGetName(CSSymbolRef symbol); + +CSRange +CSSymbolGetRange(CSSymbolRef symbol); + +const char* +CSSourceInfoGetFilename(CSSourceInfoRef info); + +uint32_t +CSSourceInfoGetLineNumber(CSSourceInfoRef info); + +CSTypeRef +CSRetain(CSTypeRef); + +void +CSRelease(CSTypeRef); + +bool +CSIsNull(CSTypeRef); + +void +CSShow(CSTypeRef); + +const char* +CSArchitectureGetFamilyName(CSArchitecture); + +} // extern "C" + +class nsCocoaDebugUtils +{ +public: + // Like NSLog() but records more information (for example the full path to + // the executable and the "thread name"). Like NSLog(), writes to both + // stdout and the system log. + static void DebugLog(const char* aFormat, ...); + + // Logs a stack trace of the current point of execution, to both stdout and + // the system log. + static void PrintStackTrace(); + + // Returns the name of the module that "owns" aAddress. This must be + // free()ed by the caller. + static char* GetOwnerName(void* aAddress); + + // Returns a symbolicated representation of aAddress. This must be + // free()ed by the caller. + static char* GetAddressString(void* aAddress); + +private: + static void DebugLogInt(bool aDecorate, const char* aFormat, ...); + static void DebugLogV(bool aDecorate, CFStringRef aFormat, va_list aArgs); + + static void PrintAddress(void* aAddress); + + // The values returned by GetOwnerNameInt() and GetAddressStringInt() must + // be free()ed by the caller. + static char* GetOwnerNameInt(void* aAddress, + CSTypeRef aOwner = sInitializer); + static char* GetAddressStringInt(void* aAddress, + CSTypeRef aOwner = sInitializer); + + static CSSymbolicatorRef GetSymbolicatorRef(); + static void ReleaseSymbolicator(); + + static CSTypeRef sInitializer; + static CSSymbolicatorRef sSymbolicator; +}; + +#endif // nsCocoaDebugUtils_h_ diff --git a/widget/cocoa/nsCocoaDebugUtils.mm b/widget/cocoa/nsCocoaDebugUtils.mm new file mode 100644 index 000000000..35896dc40 --- /dev/null +++ b/widget/cocoa/nsCocoaDebugUtils.mm @@ -0,0 +1,284 @@ +/* -*- 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 "nsCocoaDebugUtils.h" + +#include <pthread.h> +#include <libproc.h> +#include <stdarg.h> +#include <time.h> +#include <execinfo.h> +#include <asl.h> + +static char gProcPath[PROC_PIDPATHINFO_MAXSIZE] = {0}; +static char gBundleID[MAXPATHLEN] = {0}; + +static void MaybeGetPathAndID() +{ + if (!gProcPath[0]) { + proc_pidpath(getpid(), gProcPath, sizeof(gProcPath)); + } + if (!gBundleID[0]) { + // Apple's CFLog() uses "com.apple.console" (in its call to asl_open()) if + // it can't find the bundle id. + CFStringRef bundleID = NULL; + CFBundleRef mainBundle = CFBundleGetMainBundle(); + if (mainBundle) { + bundleID = CFBundleGetIdentifier(mainBundle); + } + if (!bundleID) { + strcpy(gBundleID, "com.apple.console"); + } else { + CFStringGetCString(bundleID, gBundleID, sizeof(gBundleID), + kCFStringEncodingUTF8); + } + } +} + +static void GetThreadName(char* aName, size_t aSize) +{ + pthread_getname_np(pthread_self(), aName, aSize); +} + +void +nsCocoaDebugUtils::DebugLog(const char* aFormat, ...) +{ + va_list args; + va_start(args, aFormat); + CFStringRef formatCFSTR = + CFStringCreateWithCString(kCFAllocatorDefault, aFormat, + kCFStringEncodingUTF8); + DebugLogV(true, formatCFSTR, args); + CFRelease(formatCFSTR); + va_end(args); +} + +void +nsCocoaDebugUtils::DebugLogInt(bool aDecorate, const char* aFormat, ...) +{ + va_list args; + va_start(args, aFormat); + CFStringRef formatCFSTR = + CFStringCreateWithCString(kCFAllocatorDefault, aFormat, + kCFStringEncodingUTF8); + DebugLogV(aDecorate, formatCFSTR, args); + CFRelease(formatCFSTR); + va_end(args); +} + +void +nsCocoaDebugUtils::DebugLogV(bool aDecorate, CFStringRef aFormat, + va_list aArgs) +{ + MaybeGetPathAndID(); + + CFStringRef message = + CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, NULL, + aFormat, aArgs); + + int msgLength = + CFStringGetMaximumSizeForEncoding(CFStringGetLength(message), + kCFStringEncodingUTF8); + char* msgUTF8 = (char*) calloc(msgLength, 1); + CFStringGetCString(message, msgUTF8, msgLength, kCFStringEncodingUTF8); + CFRelease(message); + + int finishedLength = msgLength + PROC_PIDPATHINFO_MAXSIZE; + char* finished = (char*) calloc(finishedLength, 1); + const time_t currentTime = time(NULL); + char timestamp[30] = {0}; + ctime_r(¤tTime, timestamp); + if (aDecorate) { + char threadName[MAXPATHLEN] = {0}; + GetThreadName(threadName, sizeof(threadName)); + snprintf(finished, finishedLength, "(%s) %s[%u] %s[%p] %s\n", + timestamp, gProcPath, getpid(), threadName, pthread_self(), msgUTF8); + } else { + snprintf(finished, finishedLength, "%s\n", msgUTF8); + } + free(msgUTF8); + + fputs(finished, stdout); + + // Use the Apple System Log facility, as NSLog and CFLog do. + aslclient asl = asl_open(NULL, gBundleID, ASL_OPT_NO_DELAY); + aslmsg msg = asl_new(ASL_TYPE_MSG); + asl_set(msg, ASL_KEY_LEVEL, "4"); // kCFLogLevelWarning, used by NSLog() + asl_set(msg, ASL_KEY_MSG, finished); + asl_send(asl, msg); + asl_free(msg); + asl_close(asl); + + free(finished); +} + +CSTypeRef +nsCocoaDebugUtils::sInitializer = {0}; + +CSSymbolicatorRef +nsCocoaDebugUtils::sSymbolicator = {0}; + +#define STACK_MAX 256 + +void +nsCocoaDebugUtils::PrintStackTrace() +{ + void** addresses = (void**) calloc(STACK_MAX, sizeof(void*)); + if (!addresses) { + return; + } + + CSSymbolicatorRef symbolicator = GetSymbolicatorRef(); + if (CSIsNull(symbolicator)) { + free(addresses); + return; + } + + uint32_t count = backtrace(addresses, STACK_MAX); + for (uint32_t i = 0; i < count; ++i) { + PrintAddress(addresses[i]); + } + + ReleaseSymbolicator(); + free(addresses); +} + +void +nsCocoaDebugUtils::PrintAddress(void* aAddress) +{ + const char* ownerName = "unknown"; + const char* addressString = "unknown + 0"; + + char* allocatedOwnerName = nullptr; + char* allocatedAddressString = nullptr; + + CSSymbolOwnerRef owner = {0}; + CSSymbolicatorRef symbolicator = GetSymbolicatorRef(); + + if (!CSIsNull(symbolicator)) { + owner = + CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator, + (unsigned long long) aAddress, + kCSNow); + } + if (!CSIsNull(owner)) { + ownerName = allocatedOwnerName = GetOwnerNameInt(aAddress, owner); + addressString = allocatedAddressString = GetAddressStringInt(aAddress, owner); + } + DebugLogInt(false, " (%s) %s", ownerName, addressString); + + free(allocatedOwnerName); + free(allocatedAddressString); + + ReleaseSymbolicator(); +} + +char* +nsCocoaDebugUtils::GetOwnerName(void* aAddress) +{ + return GetOwnerNameInt(aAddress); +} + +char* +nsCocoaDebugUtils::GetOwnerNameInt(void* aAddress, CSTypeRef aOwner) +{ + char* retval = (char*) calloc(MAXPATHLEN, 1); + + const char* ownerName = "unknown"; + + CSSymbolicatorRef symbolicator = GetSymbolicatorRef(); + CSTypeRef owner = aOwner; + + if (CSIsNull(owner) && !CSIsNull(symbolicator)) { + owner = + CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator, + (unsigned long long) aAddress, + kCSNow); + } + + if (!CSIsNull(owner)) { + ownerName = CSSymbolOwnerGetName(owner); + } + + snprintf(retval, MAXPATHLEN, "%s", ownerName); + ReleaseSymbolicator(); + + return retval; +} + +char* +nsCocoaDebugUtils::GetAddressString(void* aAddress) +{ + return GetAddressStringInt(aAddress); +} + +char* +nsCocoaDebugUtils::GetAddressStringInt(void* aAddress, CSTypeRef aOwner) +{ + char* retval = (char*) calloc(MAXPATHLEN, 1); + + const char* addressName = "unknown"; + unsigned long long addressOffset = 0; + + CSSymbolicatorRef symbolicator = GetSymbolicatorRef(); + CSTypeRef owner = aOwner; + + if (CSIsNull(owner) && !CSIsNull(symbolicator)) { + owner = + CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator, + (unsigned long long) aAddress, + kCSNow); + } + + if (!CSIsNull(owner)) { + CSSymbolRef symbol = + CSSymbolOwnerGetSymbolWithAddress(owner, + (unsigned long long) aAddress); + if (!CSIsNull(symbol)) { + addressName = CSSymbolGetName(symbol); + CSRange range = CSSymbolGetRange(symbol); + addressOffset = (unsigned long long) aAddress - range.location; + } else { + addressOffset = (unsigned long long) + aAddress - CSSymbolOwnerGetBaseAddress(owner); + } + } + + snprintf(retval, MAXPATHLEN, "%s + 0x%llx", + addressName, addressOffset); + ReleaseSymbolicator(); + + return retval; +} + +CSSymbolicatorRef +nsCocoaDebugUtils::GetSymbolicatorRef() +{ + if (CSIsNull(sSymbolicator)) { + // 0x40e0000 is the value returned by + // uint32_t CSSymbolicatorGetFlagsForNListOnlyData(void). We don't use + // this method directly because it doesn't exist on OS X 10.6. Unless + // we limit ourselves to NList data, it will take too long to get a + // stack trace where Dwarf debugging info is available (about 15 seconds + // with Firefox). This means we won't be able to get a CSSourceInfoRef, + // or line number information. Oh well. + sSymbolicator = + CSSymbolicatorCreateWithPidFlagsAndNotification(getpid(), + 0x40e0000, 0); + } + // Retaining just after creation prevents crashes when calling symbolicator + // code (for example from PrintStackTrace()) as Firefox is quitting. Not + // sure why. Doing this may mean that we leak sSymbolicator on quitting + // (if we ever created it). No particular harm in that, though. + return CSRetain(sSymbolicator); +} + +void +nsCocoaDebugUtils::ReleaseSymbolicator() +{ + if (!CSIsNull(sSymbolicator)) { + CSRelease(sSymbolicator); + } +} diff --git a/widget/cocoa/nsCocoaFeatures.h b/widget/cocoa/nsCocoaFeatures.h new file mode 100644 index 000000000..597aff611 --- /dev/null +++ b/widget/cocoa/nsCocoaFeatures.h @@ -0,0 +1,42 @@ +/* -*- 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 OSXVersion(); + static int32_t OSXVersionMajor(); + static int32_t OSXVersionMinor(); + static int32_t OSXVersionBugFix(); + static bool OnYosemiteOrLater(); + static bool OnElCapitanOrLater(); + static bool OnSierraOrLater(); + + static bool IsAtLeastVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix=0); + + // These are utilities that do not change or depend on the value of mOSXVersion + // 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: + static void InitializeVersionNumbers(); + + static int32_t mOSXVersion; +}; +#endif // nsCocoaFeatures_h_ diff --git a/widget/cocoa/nsCocoaFeatures.mm b/widget/cocoa/nsCocoaFeatures.mm new file mode 100644 index 000000000..5a5c16fa1 --- /dev/null +++ b/widget/cocoa/nsCocoaFeatures.mm @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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 OS X. +// We are assuming that the minor and bugfix versions are less than 16. +// There are MOZ_ASSERTs for that. + +// The formula for the version integer based on OS X version 10.minor.bugfix is +// 0x1000 + (minor << 4) + bugifix. See AssembleVersion() below for major > 10. +// Major version < 10 is not allowed. + +#define MAC_OS_X_VERSION_MASK 0x0000FFFF +#define MAC_OS_X_VERSION_10_0_HEX 0x00001000 +#define MAC_OS_X_VERSION_10_7_HEX 0x00001070 +#define MAC_OS_X_VERSION_10_8_HEX 0x00001080 +#define MAC_OS_X_VERSION_10_9_HEX 0x00001090 +#define MAC_OS_X_VERSION_10_10_HEX 0x000010A0 +#define MAC_OS_X_VERSION_10_11_HEX 0x000010B0 +#define MAC_OS_X_VERSION_10_12_HEX 0x000010C0 + +#include "nsCocoaFeatures.h" +#include "nsCocoaUtils.h" +#include "nsDebug.h" +#include "nsObjCExceptions.h" + +#import <Cocoa/Cocoa.h> + +int32_t nsCocoaFeatures::mOSXVersion = 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 MAC_OS_X_VERSION_10_0_HEX + (aMajor-10) * 0x100 + (aMinor << 4) + aBugFix; +} + +int32_t nsCocoaFeatures::ExtractMajorVersion(int32_t aVersion) +{ + MOZ_ASSERT((aVersion & MAC_OS_X_VERSION_MASK) == aVersion); + return ((aVersion & 0xFF00) - 0x1000) / 0x100 + 10; +} + +int32_t nsCocoaFeatures::ExtractMinorVersion(int32_t aVersion) +{ + MOZ_ASSERT((aVersion & MAC_OS_X_VERSION_MASK) == aVersion); + return (aVersion & 0xF0) >> 4; +} + +int32_t nsCocoaFeatures::ExtractBugFixVersion(int32_t aVersion) +{ + MOZ_ASSERT((aVersion & MAC_OS_X_VERSION_MASK) == aVersion); + return aVersion & 0x0F; +} + +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"]; + 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 osxVersion; + if (aMajor < 10) { + aMajor = 10; + NS_ERROR("Couldn't determine OS X version, assuming 10.7"); + osxVersion = MAC_OS_X_VERSION_10_7_HEX; + } else if (aMinor < 7) { + aMinor = 7; + NS_ERROR("OS X version too old, assuming 10.7"); + osxVersion = MAC_OS_X_VERSION_10_7_HEX; + } else { + MOZ_ASSERT(aMajor == 10); // For now, even though we're ready... + MOZ_ASSERT(aMinor < 16); + MOZ_ASSERT(aBugFix >= 0); + MOZ_ASSERT(aBugFix < 16); + osxVersion = AssembleVersion(aMajor, aMinor, aBugFix); + } + MOZ_ASSERT(aMajor == ExtractMajorVersion(osxVersion)); + MOZ_ASSERT(aMinor == ExtractMinorVersion(osxVersion)); + MOZ_ASSERT(aBugFix == ExtractBugFixVersion(osxVersion)); + return osxVersion; +} + +/*static*/ void +nsCocoaFeatures::InitializeVersionNumbers() +{ + NS_OBJC_BEGIN_TRY_ABORT_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); + mOSXVersion = GetVersion(major, minor, bugfix); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +/* static */ int32_t +nsCocoaFeatures::OSXVersion() +{ + // Don't let this be called while we're first setting the value... + MOZ_ASSERT((mOSXVersion & MAC_OS_X_VERSION_MASK) >= 0); + if (!mOSXVersion) { + mOSXVersion = -1; + InitializeVersionNumbers(); + } + return mOSXVersion; +} + +/* static */ int32_t +nsCocoaFeatures::OSXVersionMajor() +{ + MOZ_ASSERT((OSXVersion() & MAC_OS_X_VERSION_10_0_HEX) == MAC_OS_X_VERSION_10_0_HEX); + return 10; +} + +/* static */ int32_t +nsCocoaFeatures::OSXVersionMinor() +{ + return ExtractMinorVersion(OSXVersion()); +} + +/* static */ int32_t +nsCocoaFeatures::OSXVersionBugFix() +{ + return ExtractBugFixVersion(OSXVersion()); +} + +/* static */ bool +nsCocoaFeatures::OnYosemiteOrLater() +{ + return (OSXVersion() >= MAC_OS_X_VERSION_10_10_HEX); +} + +/* static */ bool +nsCocoaFeatures::OnElCapitanOrLater() +{ + return (OSXVersion() >= MAC_OS_X_VERSION_10_11_HEX); +} + +/* static */ bool +nsCocoaFeatures::OnSierraOrLater() +{ + return (OSXVersion() >= MAC_OS_X_VERSION_10_12_HEX); +} + +/* static */ bool +nsCocoaFeatures::IsAtLeastVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix) +{ + return OSXVersion() >= GetVersion(aMajor, aMinor, aBugFix); +} diff --git a/widget/cocoa/nsCocoaUtils.h b/widget/cocoa/nsCocoaUtils.h new file mode 100644 index 000000000..139e76b4a --- /dev/null +++ b/widget/cocoa/nsCocoaUtils.h @@ -0,0 +1,389 @@ +/* -*- 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 "nsRect.h" +#include "imgIContainer.h" +#include "npapi.h" +#include "nsTArray.h" +#include "Units.h" + +// This must be the last include: +#include "nsObjCExceptions.h" + +#include "mozilla/EventForwards.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 + +#if !defined(MAC_OS_X_VERSION_10_8) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 +enum { + NSEventPhaseMayBegin = 0x1 << 5 +}; +#endif + +class nsIWidget; + +namespace mozilla { +namespace gfx { +class SourceSurface; +} // namespace gfx +} // namespace mozilla + +// Used to retain a Cocoa object for the remainder of a method's execution. +class nsAutoRetainCocoaObject { +public: +explicit nsAutoRetainCocoaObject(id anObject) +{ + mObject = NS_OBJC_TRY_EXPR_ABORT([anObject retain]); +} +~nsAutoRetainCocoaObject() +{ + NS_OBJC_TRY_ABORT([mObject release]); +} +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; + +// It's sometimes necessary to explicitly remove a window from the "window +// cache" in order to deactivate it. The "window cache" is an undocumented +// subsystem, all of whose methods are included in the NSWindowCache category +// of the NSApplication class (in header files generated using class-dump). +// Present in all versions of OS X from (at least) 10.2.8 through 10.5. +- (void)_removeWindowFromCache:(NSWindow *)aWindow; + +// 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 + +class nsCocoaUtils +{ + typedef mozilla::gfx::SourceSurface SourceSurface; + typedef mozilla::LayoutDeviceIntPoint LayoutDeviceIntPoint; + typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect; + +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); + + // 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); + + // Compatibility wrappers for the -[NSEvent phase], -[NSEvent momentumPhase], + // -[NSEvent hasPreciseScrollingDeltas] and -[NSEvent scrollingDeltaX/Y] APIs + // that became availaible starting with the 10.7 SDK. + // All of these can be removed once we drop support for 10.6. + static NSEventPhase EventPhase(NSEvent* aEvent); + static NSEventPhase EventMomentumPhase(NSEvent* aEvent); + static BOOL IsMomentumScrollEvent(NSEvent* aEvent); + static BOOL HasPreciseScrollingDeltas(NSEvent* aEvent); + static void GetScrollingDeltas(NSEvent* aEvent, CGFloat* aOutDeltaX, CGFloat* aOutDeltaY); + 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 + @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise + */ + static nsresult CreateCGImageFromSurface(SourceSurface* aSurface, + CGImageRef* aResult); + + /** 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 aResult the resulting NSImage + @param scaleFactor the desired scale factor of the NSImage (2 for a retina display) + @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise + */ + static nsresult CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult, CGFloat scaleFactor); + + /** + * Returns nsAString for aSrc. + */ + static void GetStringForNSString(const NSString *aSrc, nsAString& aDist); + + /** + * Makes NSString instance for aString. + */ + static NSString* ToNSString(const nsAString& aString); + + /** + * 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); + + /** + * Initializes aNPCocoaEvent. + */ + static void InitNPCocoaEvent(NPCocoaEvent* aNPCocoaEvent); + + /** + * 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); + + /** + * Convert string with font attribute to NSMutableAttributedString + */ + static NSMutableAttributedString* GetNSMutableAttributedString( + const nsAString& aText, + const nsTArray<mozilla::FontRange>& aFontRanges, + const bool aIsVertical, + const CGFloat aBackingScaleFactor); +}; + +#endif // nsCocoaUtils_h_ diff --git a/widget/cocoa/nsCocoaUtils.mm b/widget/cocoa/nsCocoaUtils.mm new file mode 100644 index 000000000..3138245aa --- /dev/null +++ b/widget/cocoa/nsCocoaUtils.mm @@ -0,0 +1,1022 @@ +/* -*- 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 <cmath> + +#include "gfx2DGlue.h" +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "ImageRegion.h" +#include "nsCocoaUtils.h" +#include "nsChildView.h" +#include "nsMenuBarX.h" +#include "nsCocoaWindow.h" +#include "nsCOMPtr.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIAppShellService.h" +#include "nsIXULWindow.h" +#include "nsIBaseWindow.h" +#include "nsIServiceManager.h" +#include "nsMenuUtilsX.h" +#include "nsToolkit.h" +#include "nsCRT.h" +#include "SVGImageContext.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" + +using namespace mozilla; +using namespace mozilla::widget; + +using mozilla::gfx::BackendType; +using mozilla::gfx::DataSourceSurface; +using mozilla::gfx::DrawTarget; +using mozilla::gfx::Factory; +using mozilla::gfx::SamplingFilter; +using mozilla::gfx::IntPoint; +using mozilla::gfx::IntRect; +using mozilla::gfx::IntSize; +using mozilla::gfx::SurfaceFormat; +using mozilla::gfx::SourceSurface; +using mozilla::image::ImageRegion; +using std::ceil; + +static float +MenuBarScreenHeight() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSArray* allScreens = [NSScreen screens]; + if ([allScreens count]) { + return [[allScreens objectAtIndex:0] frame].size.height; + } + + return 0.0; + + NS_OBJC_END_TRY_ABORT_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); +} + +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_ABORT_BLOCK_RETURN; + + // Don't trust mouse locations of mouse move events, see bug 443178. + if (!anEvent || [anEvent type] == NSMouseMoved) + 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_ABORT_BLOCK_RETURN(NSMakePoint(0.0, 0.0)); +} + +BOOL nsCocoaUtils::IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return NSPointInRect(ScreenLocationForEvent(anEvent), [aWindow frame]); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +NSPoint nsCocoaUtils::EventLocationForWindow(NSEvent* anEvent, NSWindow* aWindow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return nsCocoaUtils::ConvertPointFromScreen(aWindow, ScreenLocationForEvent(anEvent)); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSMakePoint(0.0, 0.0)); +} + +@interface NSEvent (ScrollPhase) +// 10.5 and 10.6 +- (long long)_scrollPhase; +// 10.7 and above +- (NSEventPhase)phase; +- (NSEventPhase)momentumPhase; +@end + +NSEventPhase nsCocoaUtils::EventPhase(NSEvent* aEvent) +{ + if ([aEvent respondsToSelector:@selector(phase)]) { + return [aEvent phase]; + } + return NSEventPhaseNone; +} + +NSEventPhase nsCocoaUtils::EventMomentumPhase(NSEvent* aEvent) +{ + if ([aEvent respondsToSelector:@selector(momentumPhase)]) { + return [aEvent momentumPhase]; + } + if ([aEvent respondsToSelector:@selector(_scrollPhase)]) { + switch ([aEvent _scrollPhase]) { + case 1: return NSEventPhaseBegan; + case 2: return NSEventPhaseChanged; + case 3: return NSEventPhaseEnded; + default: return NSEventPhaseNone; + } + } + return NSEventPhaseNone; +} + +BOOL nsCocoaUtils::IsMomentumScrollEvent(NSEvent* aEvent) +{ + return [aEvent type] == NSScrollWheel && + EventMomentumPhase(aEvent) != NSEventPhaseNone; +} + +@interface NSEvent (HasPreciseScrollingDeltas) +// 10.7 and above +- (BOOL)hasPreciseScrollingDeltas; +// For 10.6 and below, see the comment in nsChildView.h about _eventRef +- (EventRef)_eventRef; +@end + +BOOL nsCocoaUtils::HasPreciseScrollingDeltas(NSEvent* aEvent) +{ + if ([aEvent respondsToSelector:@selector(hasPreciseScrollingDeltas)]) { + return [aEvent hasPreciseScrollingDeltas]; + } + + // For events that don't contain pixel scrolling information, the event + // kind of their underlaying carbon event is kEventMouseWheelMoved instead + // of kEventMouseScroll. + EventRef carbonEvent = [aEvent _eventRef]; + return carbonEvent && ::GetEventKind(carbonEvent) == kEventMouseScroll; +} + +@interface NSEvent (ScrollingDeltas) +// 10.6 and below +- (CGFloat)deviceDeltaX; +- (CGFloat)deviceDeltaY; +// 10.7 and above +- (CGFloat)scrollingDeltaX; +- (CGFloat)scrollingDeltaY; +@end + +void nsCocoaUtils::GetScrollingDeltas(NSEvent* aEvent, CGFloat* aOutDeltaX, CGFloat* aOutDeltaY) +{ + if ([aEvent respondsToSelector:@selector(scrollingDeltaX)]) { + *aOutDeltaX = [aEvent scrollingDeltaX]; + *aOutDeltaY = [aEvent scrollingDeltaY]; + return; + } + if ([aEvent respondsToSelector:@selector(deviceDeltaX)] && + HasPreciseScrollingDeltas(aEvent)) { + // Calling deviceDeltaX/Y on those events that do not contain pixel + // scrolling information triggers a Cocoa assertion and an + // Objective-C NSInternalInconsistencyException. + *aOutDeltaX = [aEvent deviceDeltaX]; + *aOutDeltaY = [aEvent deviceDeltaY]; + return; + } + + // This is only hit pre-10.7 when we are called on a scroll event that does + // not contain pixel scrolling information. + CGFloat lineDeltaPixels = 12; + *aOutDeltaX = [aEvent deltaX] * lineDeltaPixels; + *aOutDeltaY = [aEvent deltaY] * lineDeltaPixels; +} + +BOOL nsCocoaUtils::EventHasPhaseInformation(NSEvent* aEvent) +{ + if (![aEvent respondsToSelector:@selector(phase)]) { + return NO; + } + return EventPhase(aEvent) != NSEventPhaseNone || + EventMomentumPhase(aEvent) != NSEventPhaseNone; +} + +void nsCocoaUtils::HideOSChromeOnScreen(bool aShouldHide) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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<nsIXULWindow> 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 (nsIXULWindow)"); + 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_ABORT_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_ABORT_BLOCK; +} + +void nsCocoaUtils::CleanUpAfterNativeAppModalDialog() +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +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(); + } +} + +nsresult nsCocoaUtils::CreateCGImageFromSurface(SourceSurface* aSurface, + CGImageRef* aResult) +{ + 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 + + // 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_ABORT_BLOCK_NSRESULT; + + // 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_ABORT_BLOCK_NSRESULT; +} + +nsresult nsCocoaUtils::CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult, CGFloat scaleFactor) +{ + 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 && scaleFactor != 1.0f) { + 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); + + mozilla::image::DrawResult res = + aImage->Draw(context, scaledSize, ImageRegion::Create(scaledSize), + aWhichFrame, SamplingFilter::POINT, + /* no SVGImageContext */ Nothing(), + imgIContainer::FLAG_SYNC_DECODE); + + if (res != mozilla::image::DrawResult::SUCCESS) { + return NS_ERROR_FAILURE; + } + + surface = drawTarget->Snapshot(); + } else { + surface = aImage->GetFrame(aWhichFrame, imgIContainer::FLAG_SYNC_DECODE); + } + + NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE); + + CGImageRef imageRef = NULL; + nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef); + 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; +} + +// static +void +nsCocoaUtils::GetStringForNSString(const NSString *aSrc, nsAString& aDist) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!aSrc) { + aDist.Truncate(); + return; + } + + aDist.SetLength([aSrc length]); + [aSrc getCharacters: reinterpret_cast<unichar*>(aDist.BeginWriting())]; + + NS_OBJC_END_TRY_ABORT_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 +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_ABORT_BLOCK_NIL; + + NSEvent* newEvent = + [NSEvent keyEventWithType:aEventType + location:[aEvent locationInWindow] + modifierFlags:[aEvent modifierFlags] + timestamp:[aEvent timestamp] + windowNumber:[aEvent windowNumber] + context:[aEvent context] + characters:[aEvent characters] + charactersIgnoringModifiers:[aEvent charactersIgnoringModifiers] + isARepeat:[aEvent isARepeat] + keyCode:[aEvent keyCode]]; + return newEvent; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// static +void +nsCocoaUtils::InitNPCocoaEvent(NPCocoaEvent* aNPCocoaEvent) +{ + memset(aNPCocoaEvent, 0, sizeof(NPCocoaEvent)); +} + +// static +void +nsCocoaUtils::InitInputEvent(WidgetInputEvent& aInputEvent, + NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + aInputEvent.mModifiers = ModifiersForEvent(aNativeEvent); + aInputEvent.mTime = PR_IntervalNow(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// static +Modifiers +nsCocoaUtils::ModifiersForEvent(NSEvent* aNativeEvent) +{ + NSUInteger modifiers = + aNativeEvent ? [aNativeEvent modifierFlags] : [NSEvent modifierFlags]; + Modifiers result = 0; + if (modifiers & NSShiftKeyMask) { + result |= MODIFIER_SHIFT; + } + if (modifiers & NSControlKeyMask) { + result |= MODIFIER_CONTROL; + } + if (modifiers & NSAlternateKeyMask) { + 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 & NSCommandKeyMask) { + result |= MODIFIER_META; + } + + if (modifiers & NSAlphaShiftKeyMask) { + 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 & NSNumericPadKeyMask) { + result |= MODIFIER_NUMLOCK; + } + + // Be aware, NSFunctionKeyMask 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 & NSAlphaShiftKeyMask) { + carbonModifier |= alphaLock; + } + if (aCocoaModifier & NSControlKeyMask) { + carbonModifier |= controlKey; + } + if (aCocoaModifier & NSAlternateKeyMask) { + carbonModifier |= optionKey; + } + if (aCocoaModifier & NSShiftKeyMask) { + carbonModifier |= shiftKey; + } + if (aCocoaModifier & NSCommandKeyMask) { + carbonModifier |= cmdKey; + } + if (aCocoaModifier & NSNumericPadKeyMask) { + carbonModifier |= kEventKeyModifierNumLockMask; + } + if (aCocoaModifier & NSFunctionKeyMask) { + 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; + } + CGFloat scale = + [screen respondsToSelector:@selector(backingScaleFactor)] ? + [screen backingScaleFactor] : 1.0; + // Currently, we only care about differentiating "1.0" and "2.0", + // so we set one of the two low bits to record which. + if (scale > 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; +} + +void +nsCocoaUtils::GetCommandsFromKeyEvent(NSEvent* aEvent, + nsTArray<KeyBindingsCommand>& aCommands) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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 nsIDOMKeyEvent 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; + keyCodeName.AssignWithConversion(aKeyCodeName); + // 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; +} + +NSMutableAttributedString* +nsCocoaUtils::GetNSMutableAttributedString( + const nsAString& aText, + const nsTArray<mozilla::FontRange>& aFontRanges, + const bool aIsVertical, + const CGFloat aBackingScaleFactor) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL + + 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_ABORT_BLOCK_NIL +} diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h new file mode 100644 index 000000000..6338f474d --- /dev/null +++ b/widget/cocoa/nsCocoaWindow.h @@ -0,0 +1,423 @@ +/* -*- 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" + +class nsCocoaWindow; +class nsChildView; +class nsMenuBarX; +@class ChildView; + +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; + NSColor* mActiveTitlebarColor; + NSColor* mInactiveTitlebarColor; + + // Shadow + BOOL mScheduledShadowInvalidation; + + // Invalidation disabling + BOOL mDisabledNeedsDisplay; + + // DPI cache. Getting the physical screen size (CGDisplayScreenSize) + // is ridiculously slow, so we cache it in the toplevel window for all + // descendants to use. + float mDPI; + + NSTrackingArea* mTrackingArea; + + NSRect mDirtyRect; + + BOOL mBeingShown; + BOOL mDrawTitle; + BOOL mBrightTitlebarForeground; + BOOL mUseMenuStyle; +} + +- (void)importState:(NSDictionary*)aState; +- (NSMutableDictionary*)exportState; +- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState; +- (BOOL)drawsContentsIntoWindowFrame; +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive; +- (NSColor*)titlebarColorForActiveWindow:(BOOL)aActive; + +- (void)deferredInvalidateShadow; +- (void)invalidateShadow; +- (float)getDPI; + +- (void)mouseEntered:(NSEvent*)aEvent; +- (void)mouseExited:(NSEvent*)aEvent; +- (void)mouseMoved:(NSEvent*)aEvent; +- (void)updateTrackingArea; +- (NSView*)trackingAreaView; + +- (void)setBeingShown:(BOOL)aValue; +- (BOOL)isBeingShown; +- (BOOL)isVisibleOrBeingShown; + +- (ChildView*)mainChildView; + +- (NSArray*)titlebarControls; + +- (void)setWantsTitleDrawn:(BOOL)aDrawTitle; +- (BOOL)wantsTitleDrawn; + +- (void)setUseBrightTitlebarForeground:(BOOL)aBrightForeground; +- (BOOL)useBrightTitlebarForeground; + +- (void)disableSetNeedsDisplay; +- (void)enableSetNeedsDisplay; + +- (NSRect)getAndResetNativeDirtyRect; + +- (void)setUseMenuStyle:(BOOL)aValue; + +@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; + +// If we set the window's stylemask to be textured, the corners on the bottom of +// the window are rounded by default. We use this private method to make +// the corners square again, a la Safari. Starting with 10.7, all windows have +// rounded bottom corners, so this call doesn't have any effect there. +- (void)setBottomCornerRounded:(BOOL)rounded; +- (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 + +@class ToolbarWindow; + +// NSColor subclass that allows us to draw separate colors both in the titlebar +// and for background of the window. +@interface TitlebarAndBackgroundColor : NSColor +{ + ToolbarWindow *mWindow; // [WEAK] (we are owned by the window) +} + +- (id)initWithWindow:(ToolbarWindow*)aWindow; + +@end + +// NSWindow subclass for handling windows with toolbars. +@interface ToolbarWindow : BaseWindow +{ + TitlebarAndBackgroundColor *mColor; // strong + CGFloat mUnifiedToolbarHeight; + NSColor *mBackgroundColor; // strong + NSView *mTitlebarView; // strong + NSRect mWindowButtonsRect; + NSRect mFullScreenButtonRect; +} +// Pass nil here to get the default appearance. +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive; +- (void)setUnifiedToolbarHeight:(CGFloat)aHeight; +- (CGFloat)unifiedToolbarHeight; +- (CGFloat)titlebarHeight; +- (NSRect)titlebarRect; +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect sync:(BOOL)aSync; +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect; +- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState; +- (void)setSheetAttachmentPosition:(CGFloat)aY; +- (void)placeWindowButtons:(NSRect)aRect; +- (void)placeFullScreenButton:(NSRect)aRect; +- (NSPoint)windowButtonsPositionWithDefaultPosition:(NSPoint)aDefaultPosition; +- (NSPoint)fullScreenButtonPositionWithDefaultPosition:(NSPoint)aDefaultPosition; +- (void)setTemporaryBackgroundColor; +- (void)restoreBackgroundColor; +@end + +class nsCocoaWindow : public nsBaseWidget, public nsPIWidgetCocoa +{ +private: + typedef nsBaseWidget Inherited; + +public: + + nsCocoaWindow(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSPIWIDGETCOCOA + + virtual MOZ_MUST_USE nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const DesktopIntRect& aRect, + nsWidgetInitData* aInitData = nullptr) + override; + + virtual MOZ_MUST_USE nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData = nullptr) + override; + + virtual void Destroy() override; + + NS_IMETHOD Show(bool aState) override; + virtual nsIWidget* GetSheetWindowParent(void) override; + NS_IMETHOD 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; + NS_IMETHOD SetFocus(bool aState=false) 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; + NS_IMETHOD Move(double aX, double aY) override; + virtual void SetSizeMode(nsSizeMode aMode) override; + NS_IMETHOD HideWindowChrome(bool aShouldHide) override; + + 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 nsresult MakeFullScreen( + bool aFullScreen, nsIScreen* aTargetScreen = nullptr) override final; + NS_IMETHOD MakeFullScreenWithNativeTransition( + bool aFullScreen, nsIScreen* aTargetScreen = nullptr) override final; + NSAnimation* FullscreenTransitionAnimation() const { return mFullscreenTransitionAnimation; } + void ReleaseFullscreenTransitionAnimation() + { + MOZ_ASSERT(mFullscreenTransitionAnimation, + "Should only be called when there is animation"); + [mFullscreenTransitionAnimation release]; + mFullscreenTransitionAnimation = nil; + } + + NS_IMETHOD Resize(double aWidth, double aHeight, bool aRepaint) override; + NS_IMETHOD Resize(double aX, double aY, double aWidth, double aHeight, bool aRepaint) override; + virtual LayoutDeviceIntRect GetClientBounds() override; + virtual LayoutDeviceIntRect GetScreenBounds() override; + void ReportMoveEvent(); + void ReportSizeEvent(); + NS_IMETHOD SetCursor(nsCursor aCursor) override; + NS_IMETHOD SetCursor(imgIContainer* aCursor, uint32_t aHotspotX, uint32_t aHotspotY) override; + + CGFloat BackingScaleFactor(); + void BackingScaleFactorChanged(); + virtual double GetDefaultScaleInternal() override; + virtual int32_t RoundsWidgetCoordinatesTo() override; + + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() final { + return mozilla::DesktopToLayoutDeviceScale(BackingScaleFactor()); + } + + NS_IMETHOD SetTitle(const nsAString& aTitle) override; + + NS_IMETHOD Invalidate(const LayoutDeviceIntRect& aRect) override; + virtual nsresult ConfigureChildren(const nsTArray<Configuration>& aConfigurations) override; + virtual LayerManager* GetLayerManager(PLayerTransactionChild* aShadowManager = nullptr, + LayersBackend aBackendHint = mozilla::layers::LayersBackend::LAYERS_NONE, + LayerManagerPersistence aPersistence = LAYER_MANAGER_CURRENT) override; + NS_IMETHOD DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override; + virtual void CaptureRollupEvents(nsIRollupListener * aListener, + bool aDoCapture) override; + NS_IMETHOD GetAttention(int32_t aCycleCount) override; + virtual bool HasPendingInputEvent() override; + virtual nsTransparencyMode GetTransparencyMode() override; + virtual void SetTransparencyMode(nsTransparencyMode aMode) override; + virtual void SetWindowShadowStyle(int32_t aStyle) override; + virtual void SetShowsToolbarButton(bool aShow) override; + virtual void SetShowsFullScreenButton(bool aShow) override; + virtual void SetWindowAnimationType(WindowAnimationType aType) override; + virtual void SetDrawsTitle(bool aDrawTitle) override; + virtual void SetUseBrightTitlebarForeground(bool aBrightForeground) override; + NS_IMETHOD SetNonClientMargins(LayoutDeviceIntMargin& aMargins) override; + virtual void SetWindowTitlebarColor(nscolor aColor, bool aActive) override; + virtual void SetDrawsInTitlebar(bool aState) override; + virtual void UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) override; + virtual nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) override; + + void DispatchSizeModeEvent(); + + // 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(nsMenuBarX* aMenuBar); + nsMenuBarX *GetMenuBar(); + + NS_IMETHOD_(void) SetInputContext( + const InputContext& aContext, + const InputContextAction& aAction) override; + NS_IMETHOD_(InputContext) GetInputContext() override + { + return mInputContext; + } + NS_IMETHOD_(bool) ExecuteNativeKeyBinding( + NativeKeyBindingsType aType, + const mozilla::WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) override; + + void SetPopupWindowLevel(); + +protected: + virtual ~nsCocoaWindow(); + + nsresult CreateNativeWindow(const NSRect &aRect, + nsBorderStyle aBorderStyle, + bool aRectIsFrameRect); + nsresult CreatePopupContentView(const LayoutDeviceIntRect &aRect); + void DestroyNativeWindow(); + void AdjustWindowShadow(); + void SetWindowBackgroundBlur(); + void UpdateBounds(); + + nsresult DoResize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint, bool aConstrainToCurrentScreen); + + inline bool ShouldToggleNativeFullscreen(bool aFullScreen, + bool aUseSystemTransition); + nsresult DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition); + + virtual already_AddRefed<nsIWidget> + AllocateChildPopupWidget() override + { + static NS_DEFINE_IID(kCPopUpCID, NS_POPUP_CID); + nsCOMPtr<nsIWidget> widget = do_CreateInstance(kCPopUpCID); + return widget.forget(); + } + + 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; + int32_t mShadowStyle; + + CGFloat mBackingScaleFactor; + + 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 + bool mInFullScreenMode; + bool mInFullScreenTransition; // true from the request to enter/exit fullscreen + // (MakeFullScreen() call) to EnteredFullScreen() + bool mModal; + bool mFakeModal; + + // Only true on 10.7+ if SetShowsFullScreenButton(true) is called. + bool mSupportsNativeFullScreen; + // 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(). + + int32_t mNumModalDescendents; + InputContext mInputContext; +}; + +#endif // nsCocoaWindow_h_ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm new file mode 100644 index 000000000..db120fbdd --- /dev/null +++ b/widget/cocoa/nsCocoaWindow.mm @@ -0,0 +1,3834 @@ +/* -*- 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 "NativeKeyBindings.h" +#include "TextInputHandler.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 "nsIXULWindow.h" +#include "nsToolkit.h" +#include "nsIDOMWindow.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMElement.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 "nsIPresShell.h" +#include "nsScreenCocoa.h" + +#include "gfxPlatform.h" +#include "qcms.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Preferences.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; + +// defined in nsMenuBarX.mm +extern NSMenu* sApplicationMenu; // Application menu shared by all menubars + +// defined in nsChildView.mm +extern BOOL gSomeMenuBarPainted; + +#if !defined(MAC_OS_X_VERSION_10_12) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 + +@interface NSWindow(AutomaticWindowTabbing) ++ (void)setAllowsAutomaticWindowTabbing:(BOOL)allow; +@end + +#endif + +extern "C" { + // CGSPrivate.h + typedef NSInteger CGSConnection; + typedef NSInteger CGSWindow; + typedef NSUInteger CGSWindowFilterRef; + extern CGSConnection _CGSDefaultConnection(void); + extern CGError CGSSetWindowShadowAndRimParameters(const CGSConnection cid, CGSWindow wid, float standardDeviation, float density, int offsetX, int offsetY, unsigned int flags); + extern CGError CGSSetWindowBackgroundBlurRadius(CGSConnection cid, CGSWindow wid, NSUInteger blur); +} + +#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); + 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(NS_STYLE_WINDOW_SHADOW_DEFAULT) +, mBackingScaleFactor(0.0) +, mAnimationType(nsIWidget::eGenericWindowAnimation) +, mWindowMadeHere(false) +, mSheetNeedsShow(false) +, mInFullScreenMode(false) +, mInFullScreenTransition(false) +, mModal(false) +, mFakeModal(false) +, mSupportsNativeFullScreen(false) +, mInNativeFullScreenMode(false) +, mIsAnimationSuppressed(false) +, mInReportMoveEvent(false) +, mInResize(false) +, mNumModalDescendents(0) +{ + if ([NSWindow respondsToSelector:@selector(setAllowsAutomaticWindowTabbing:)]) { + // Disable automatic tabbing on 10.12. We need to do this before we + // orderFront any of our windows. + [NSWindow setAllowsAutomaticWindowTabbing:NO]; + } +} + +void nsCocoaWindow::DestroyNativeWindow() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // 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_ABORT_BLOCK; +} + +nsCocoaWindow::~nsCocoaWindow() +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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; + } +} + +// Some applications use native popup windows +// (native context menus, native tooltips) +static bool UseNativePopupWindows() +{ +#ifdef MOZ_USE_NATIVE_POPUP_WINDOWS + return true; +#else + return false; +#endif /* MOZ_USE_NATIVE_POPUP_WINDOWS */ +} + +// aRect here is specified in desktop pixels +nsresult +nsCocoaWindow::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const DesktopIntRect& aRect, + nsWidgetInitData* aInitData) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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; + + // Applications that use native popups don't want us to create popup windows. + if ((mWindowType == eWindowType_popup) && UseNativePopupWindows()) + return NS_OK; + + nsresult rv = + CreateNativeWindow(nsCocoaUtils::GeckoRectToCocoaRect(newBounds), + mBorderStyle, false); + NS_ENSURE_SUCCESS(rv, rv); + + if (mWindowType == eWindowType_popup) { + if (aInitData->mMouseTransparent) { + [mWindow setIgnoresMouseEvents:YES]; + } + // now we can convert newBounds 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); + } + + mIsAnimationSuppressed = aInitData->mIsAnimationSuppressed; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult +nsCocoaWindow::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) +{ + DesktopIntRect desktopRect = + RoundedToInt(aRect / GetDesktopToDeviceScale()); + 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 NSTitledWindowMask if it includes any of the others [besides + * NSBorderlessWindowMask]". This implies that a borderless window + * shouldn't have any other styles than NSBorderlessWindowMask. + */ + if (!allOrDefault && !(aBorderStyle & eBorderStyle_title)) + return NSBorderlessWindowMask; + + unsigned int mask = NSTitledWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_close) + mask |= NSClosableWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_minimize) + mask |= NSMiniaturizableWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_resizeh) + mask |= NSResizableWindowMask; + + 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_ABORT_BLOCK_NSRESULT; + + // We default to NSBorderlessWindowMask, add features if needed. + unsigned int features = NSBorderlessWindowMask; + + // Configure the window we will create based on the window type. + switch (mWindowType) + { + case eWindowType_invisible: + case eWindowType_child: + case eWindowType_plugin: + break; + case eWindowType_popup: + if (aBorderStyle != eBorderStyle_default && mBorderStyle & eBorderStyle_title) { + features |= NSTitledWindowMask; + if (aBorderStyle & eBorderStyle_close) { + features |= NSClosableWindowMask; + } + } + break; + case eWindowType_toplevel: + case eWindowType_dialog: + features = WindowMaskForBorderStyle(aBorderStyle); + break; + case eWindowType_sheet: + if (mParent->WindowType() != eWindowType_invisible && + aBorderStyle & eBorderStyle_resizeh) { + features = NSResizableWindowMask; + } + else { + features = NSMiniaturizableWindowMask; + } + features |= NSTitledWindowMask; + 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 & NSTitledWindowMask)) + 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 == NSBorderlessWindowMask) + windowClass = [BorderlessWindow class]; + + // Create the window + mWindow = [[windowClass alloc] initWithContentRect:contentRect styleMask:features + backing:NSBackingStoreBuffered defer:YES]; + + // 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 frameRectForContentRect: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 setHasShadow:YES]; + [mWindow setBackgroundColor:[NSColor clearColor]]; + [mWindow setOpaque:NO]; + } else { + // Make sure that regular windows are opaque from the start, so that + // nsChildView::WidgetTypeSupportsAcceleration returns true for them. + [mWindow setOpaque:YES]; + } + + [mWindow setContentMinSize:NSMakeSize(60, 60)]; + [mWindow disableCursorRects]; + + // 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; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsCocoaWindow::CreatePopupContentView(const LayoutDeviceIntRect &aRect) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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, + nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ChildView* newContentView = (ChildView*)mPopupContentView->GetNativeData(NS_NATIVE_WIDGET); + [mWindow setContentView:newContentView]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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_ABORT_BLOCK_NSNULL; + + 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_ABORT_BLOCK_NSNULL; +} + +bool nsCocoaWindow::IsVisible() const +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return (mWindow && ([mWindow isVisibleOrBeingShown] || mSheetNeedsShow)); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void +nsCocoaWindow::SetModal(bool aState) +{ + NS_OBJC_BEGIN_TRY_ABORT_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 nsXULWindow:: + // 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_ABORT_BLOCK; +} + +void +nsCocoaWindow::SetFakeModal(bool aState) +{ + mFakeModal = aState; + SetModal(aState); +} + +bool +nsCocoaWindow::IsRunningAppModal() +{ + return [NSApp _isRunningAppModal]; +} + +// Hide or show this window +NS_IMETHODIMP nsCocoaWindow::Show(bool bState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow) + return NS_OK; + + // We need to re-execute sometimes in order to bring already-visible + // windows forward. + if (!mSheetNeedsShow && !bState && ![mWindow isVisible]) + return NS_OK; + + // Protect against re-entering. + if (bState && [mWindow isBeingShown]) + return NS_OK; + + [mWindow setBeingShown:bState]; + + nsIWidget* parentWidget = mParent; + nsCOMPtr<nsPIWidgetCocoa> piParentWidget(do_QueryInterface(parentWidget)); + NSWindow* nativeParentWindow = (parentWidget) ? + (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW) : nil; + + if (bState && !mBounds.IsEmpty()) { + 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 NS_ERROR_FAILURE; + + 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); + [NSApp endSheet:nativeParentWindow]; + } + + 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 + // [NSApp beginSheet...] won't match up with [NSApp endSheet...]. + if (![mWindow isVisible]) { + mSheetNeedsShow = false; + mSheetWindowParent = topNonSheetWindow; + // 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]; + [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) { + // If a popup window is shown after being hidden, it needs to be "reset" + // for it to receive any mouse events aside from mouse-moved events + // (because it was removed from the "window cache" when it was hidden + // -- see below). Setting the window number to -1 and then back to its + // original value seems to accomplish this. The idea was "borrowed" + // from the Java Embedding Plugin. + NSInteger windowNumber = [mWindow windowNumber]; + [mWindow _setWindowNumber:-1]; + [mWindow _setWindowNumber:windowNumber]; + // 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_ABORT_BLOCK; + [[mWindow contentView] setNeedsDisplay:YES]; + [mWindow orderFront:nil]; + NS_OBJC_END_TRY_ABORT_BLOCK; + SendSetZLevelEvent(); + AdjustWindowShadow(); + SetWindowBackgroundBlur(); + // 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_ABORT_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: + NS_NOTREACHED("unexpected mAnimationType value"); + // fall through + case nsIWidget::eGenericWindowAnimation: + behavior = NSWindowAnimationBehaviorDefault; + break; + } + } + [mWindow setAnimationBehavior:behavior]; + } + [mWindow makeKeyAndOrderFront:nil]; + NS_OBJC_END_TRY_ABORT_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 + [NSApp endSheet:mWindow]; + + [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) { + // Only set contextInfo if the parent of the parent sheet we're about + // to restore isn't itself a sheet. + NSWindow* contextInfo = sheetParent; + 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) { + contextInfo = nil; + } + } + // 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. + [NSApp beginSheet:nativeParentWindow + modalForWindow:sheetParent + modalDelegate:[nativeParentWindow delegate] + didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) + contextInfo:contextInfo]; + } + else { + // Sheet, that was hard. No more siblings or parents, going back + // to a real window. + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + [sheetParent makeKeyAndOrderFront:nil]; + NS_OBJC_END_TRY_ABORT_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]; + // Unless it's explicitly removed from NSApp's "window cache", a popup + // window will keep receiving mouse-moved events even after it's been + // "ordered out" (instead of the browser window that was underneath it, + // until you click on that window). This is bmo bug 378645, but it's + // surely an Apple bug. The "window cache" is an undocumented subsystem, + // all of whose methods are included in the NSWindowCache category of + // the NSApplication class (in header files generated using class-dump). + // This workaround was "borrowed" from the Java Embedding Plugin (which + // uses it for a different purpose). + if (mWindowType == eWindowType_popup) + [NSApp _removeWindowFromCache:mWindow]; + + // 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]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +struct ShadowParams { + float standardDeviation; + float density; + int offsetX; + int offsetY; + unsigned int flags; +}; + +// These numbers have been determined by looking at the results of +// CGSGetWindowShadowAndRimParameters for native window types. +static const ShadowParams kWindowShadowParametersPreYosemite[] = { + { 0.0f, 0.0f, 0, 0, 0 }, // none + { 8.0f, 0.5f, 0, 6, 1 }, // default + { 10.0f, 0.44f, 0, 10, 512 }, // menu + { 8.0f, 0.5f, 0, 6, 1 }, // tooltip + { 4.0f, 0.6f, 0, 4, 512 } // sheet +}; + +static const ShadowParams kWindowShadowParametersPostYosemite[] = { + { 0.0f, 0.0f, 0, 0, 0 }, // none + { 8.0f, 0.5f, 0, 6, 1 }, // default + { 9.882353f, 0.3f, 0, 4, 0 }, // menu + { 3.294118f, 0.2f, 0, 1, 0 }, // tooltip + { 9.882353f, 0.3f, 0, 4, 0 } // sheet +}; + +// This method will adjust the window shadow style for popup windows after +// they have been made visible. Before they're visible, their window number +// might be -1, which is not useful. +// We won't attempt to change the shadow for windows that can acquire key state +// since OS X will reset the shadow whenever that happens. +void +nsCocoaWindow::AdjustWindowShadow() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow isVisible] || ![mWindow hasShadow] || + [mWindow canBecomeKeyWindow] || [mWindow windowNumber] == -1) + return; + + const ShadowParams& params = nsCocoaFeatures::OnYosemiteOrLater() + ? kWindowShadowParametersPostYosemite[mShadowStyle] + : kWindowShadowParametersPreYosemite[mShadowStyle]; + CGSConnection cid = _CGSDefaultConnection(); + CGSSetWindowShadowAndRimParameters(cid, [mWindow windowNumber], + params.standardDeviation, params.density, + params.offsetX, params.offsetY, + params.flags); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +static const NSUInteger kWindowBackgroundBlurRadius = 4; + +void +nsCocoaWindow::SetWindowBackgroundBlur() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow isVisible] || [mWindow windowNumber] == -1) + return; + + // Only blur the background of menus and fake sheets. + if (mShadowStyle != NS_STYLE_WINDOW_SHADOW_MENU && + mShadowStyle != NS_STYLE_WINDOW_SHADOW_SHEET) + return; + + CGSConnection cid = _CGSDefaultConnection(); + CGSSetWindowBackgroundBlurRadius(cid, [mWindow windowNumber], kWindowBackgroundBlurRadius); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsresult +nsCocoaWindow::ConfigureChildren(const nsTArray<Configuration>& aConfigurations) +{ + if (mPopupContentView) { + mPopupContentView->ConfigureChildren(aConfigurations); + } + return NS_OK; +} + +LayerManager* +nsCocoaWindow::GetLayerManager(PLayerTransactionChild* aShadowManager, + LayersBackend aBackendHint, + LayerManagerPersistence aPersistence) +{ + if (mPopupContentView) { + return mPopupContentView->GetLayerManager(aShadowManager, + aBackendHint, + aPersistence); + } + return nullptr; +} + +nsTransparencyMode nsCocoaWindow::GetTransparencyMode() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return (!mWindow || [mWindow isOpaque]) ? eTransparencyOpaque : eTransparencyTransparent; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(eTransparencyOpaque); +} + +// This is called from nsMenuPopupFrame when making a popup transparent, or +// from nsChildView::SetTransparencyMode for other window types. +void nsCocoaWindow::SetTransparencyMode(nsTransparencyMode aMode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // Transparent windows are only supported on popups. + BOOL isTransparent = aMode == eTransparencyTransparent && + mWindowType == eWindowType_popup; + BOOL currentTransparency = ![mWindow isOpaque]; + if (isTransparent != currentTransparency) { + [mWindow setOpaque:!isTransparent]; + [mWindow setBackgroundColor:(isTransparent ? [NSColor clearColor] : [NSColor whiteColor])]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::Enable(bool aState) +{ + return NS_OK; +} + +bool nsCocoaWindow::IsEnabled() const +{ + return true; +} + +#define kWindowPositionSlop 20 + +void +nsCocoaWindow::ConstrainPosition(bool aAllowSlop, int32_t *aX, int32_t *aY) +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +void nsCocoaWindow::SetSizeConstraints(const SizeConstraints& aConstraints) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Popups can be smaller than (60, 60) + NSRect rect = + (mWindowType == eWindowType_popup) ? NSZeroRect : NSMakeRect(0.0, 0.0, 60, 60); + rect = [mWindow frameRectForContentRect:rect]; + + CGFloat scaleFactor = BackingScaleFactor(); + + SizeConstraints c = aConstraints; + c.mMinSize.width = + std::max(nsCocoaUtils::CocoaPointsToDevPixels(rect.size.width, scaleFactor), + c.mMinSize.width); + c.mMinSize.height = + std::max(nsCocoaUtils::CocoaPointsToDevPixels(rect.size.height, scaleFactor), + c.mMinSize.height); + + NSSize minSize = { + nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.width, scaleFactor), + nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.height, scaleFactor) + }; + [mWindow setMinSize:minSize]; + + NSSize maxSize = { + c.mMaxSize.width == NS_MAXSIZE ? + FLT_MAX : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.width, scaleFactor), + c.mMaxSize.height == NS_MAXSIZE ? + FLT_MAX : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.height, scaleFactor) + }; + [mWindow setMaxSize:maxSize]; + + nsBaseWidget::SetSizeConstraints(c); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Move(double aX, double aY) +{ + if (!mWindow) { + return NS_OK; + } + + // 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]; + } + + return NS_OK; +} + +void +nsCocoaWindow::SetSizeMode(nsSizeMode aMode) +{ + NS_OBJC_BEGIN_TRY_ABORT_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 (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_ABORT_BLOCK; +} + +// 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. +NS_IMETHODIMP nsCocoaWindow::HideWindowChrome(bool aShouldHide) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow || !mWindowMadeHere || + (mWindowType != eWindowType_toplevel && mWindowType != eWindowType_dialog)) + return NS_ERROR_FAILURE; + + 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 content view. + NSView* contentView = [mWindow contentView]; + [contentView retain]; + [contentView 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(rv, rv); + + // Re-import state. + [mWindow importState:state]; + + // Reparent the content view. + [mWindow setContentView:contentView]; + [contentView release]; + + // 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; + rv = Show(true); + mIsAnimationSuppressed = wasAnimationSuppressed; + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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 + +/* virtual */ bool +nsCocoaWindow::PrepareForFullscreenTransition(nsISupports** aData) +{ + nsCOMPtr<nsIScreen> widgetScreen = GetWidgetScreen(); + nsScreenCocoa* screen = static_cast<nsScreenCocoa*>(widgetScreen.get()); + NSScreen* cocoaScreen = screen->CocoaScreen(); + + NSWindow* win = + [[NSWindow alloc] initWithContentRect:[cocoaScreen frame] + styleMask:NSBorderlessWindowMask + 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::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::EnteredFullScreen(bool aFullScreen, bool aNativeMode) +{ + mInFullScreenTransition = false; + bool wasInFullscreen = mInFullScreenMode; + mInFullScreenMode = aFullScreen; + if (aNativeMode || mInNativeFullScreenMode) { + mInNativeFullScreenMode = aFullScreen; + } + DispatchSizeModeEvent(); + if (mWidgetListener && wasInFullscreen != aFullScreen) { + mWidgetListener->FullscreenChanged(aFullScreen); + } +} + +inline bool +nsCocoaWindow::ShouldToggleNativeFullscreen(bool aFullScreen, + bool aUseSystemTransition) +{ + if (!mSupportsNativeFullScreen) { + // If we cannot use native fullscreen, don't touch it. + 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, nsIScreen* aTargetScreen) +{ + return DoMakeFullScreen(aFullScreen, false); +} + +NS_IMETHODIMP +nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen, + nsIScreen* aTargetScreen) +{ + return DoMakeFullScreen(aFullScreen, true); +} + +nsresult +nsCocoaWindow::DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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)) { + // If we're using native fullscreen mode and our native window is invisible, + // our attempt to go into fullscreen mode will fail with an assertion in + // system code, without [WindowDelegate windowDidFailToEnterFullScreen:] + // ever getting called. To pre-empt this we bail here. See bug 752294. + if (aFullScreen && ![mWindow isVisible]) { + EnteredFullScreen(false); + return NS_OK; + } + 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 { + 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_ABORT_BLOCK_NSRESULT; +} + +// Coordinates are desktop pixels +nsresult nsCocoaWindow::DoResize(double aX, double aY, + double aWidth, double aHeight, + bool aRepaint, + bool aConstrainToCurrentScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow || mInResize) { + return NS_OK; + } + + AutoRestore<bool> reentrantResizeGuard(mInResize); + mInResize = true; + + // ConstrainSize operates in device pixels, so we need to convert using + // the backing scale factor here + CGFloat scale = BackingScaleFactor(); + int32_t width = NSToIntRound(aWidth * scale); + int32_t height = NSToIntRound(aHeight * scale); + ConstrainSize(&width, &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 NS_OK; + } + + // 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]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Resize(double aX, double aY, + double aWidth, double aHeight, + bool aRepaint) +{ + return DoResize(aX, aY, aWidth, aHeight, aRepaint, false); +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Resize(double aWidth, double aHeight, bool aRepaint) +{ + double invScale = 1.0 / BackingScaleFactor(); + return DoResize(mBounds.x * invScale, mBounds.y * invScale, + aWidth, aHeight, aRepaint, true); +} + +LayoutDeviceIntRect +nsCocoaWindow::GetClientBounds() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + CGFloat scaleFactor = BackingScaleFactor(); + if (!mWindow) { + return nsCocoaUtils::CocoaRectToGeckoRectDevPix(NSZeroRect, scaleFactor); + } + + NSRect r; + if ([mWindow isKindOfClass:[ToolbarWindow class]] && + [(ToolbarWindow*)mWindow drawsContentsIntoWindowFrame]) { + r = [mWindow frame]; + } else { + r = [mWindow contentRectForFrameRect:[mWindow frame]]; + } + + return nsCocoaUtils::CocoaRectToGeckoRectDevPix(r, scaleFactor); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0)); +} + +void +nsCocoaWindow::UpdateBounds() +{ + NSRect frame = NSZeroRect; + if (mWindow) { + frame = [mWindow frame]; + } + mBounds = + nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor()); +} + +LayoutDeviceIntRect +nsCocoaWindow::GetScreenBounds() +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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; + } + + if (mBackingScaleFactor > 0.0) { + // convert size constraints to the new device pixel coordinate space + double scaleFactor = newScale / mBackingScaleFactor; + mSizeConstraints.mMinSize.width = + NSToIntRound(mSizeConstraints.mMinSize.width * scaleFactor); + mSizeConstraints.mMinSize.height = + NSToIntRound(mSizeConstraints.mMinSize.height * scaleFactor); + if (mSizeConstraints.mMaxSize.width < NS_MAXSIZE) { + mSizeConstraints.mMaxSize.width = + std::min(NS_MAXSIZE, + NSToIntRound(mSizeConstraints.mMaxSize.width * scaleFactor)); + } + if (mSizeConstraints.mMaxSize.height < NS_MAXSIZE) { + mSizeConstraints.mMaxSize.height = + std::min(NS_MAXSIZE, + NSToIntRound(mSizeConstraints.mMaxSize.height * scaleFactor)); + } + } + + mBackingScaleFactor = newScale; + + if (!mWidgetListener || mWidgetListener->GetXULWindow()) { + return; + } + + nsIPresShell* presShell = mWidgetListener->GetPresShell(); + if (presShell) { + presShell->BackingScaleFactorChanged(); + } +} + +int32_t +nsCocoaWindow::RoundsWidgetCoordinatesTo() +{ + if (BackingScaleFactor() == 2.0) { + return 2; + } + return 1; +} + +NS_IMETHODIMP nsCocoaWindow::SetCursor(nsCursor aCursor) +{ + if (mPopupContentView) + return mPopupContentView->SetCursor(aCursor); + + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::SetCursor(imgIContainer* aCursor, + uint32_t aHotspotX, uint32_t aHotspotY) +{ + if (mPopupContentView) + return mPopupContentView->SetCursor(aCursor, aHotspotX, aHotspotY); + + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::SetTitle(const nsAString& aTitle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow) + return NS_OK; + + const nsString& strTitle = PromiseFlatString(aTitle); + NSString* title = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(strTitle.get()) + length:strTitle.Length()]; + + if ([mWindow drawsContentsIntoWindowFrame] && ![mWindow wantsTitleDrawn]) { + // Don't cause invalidations. + [mWindow disableSetNeedsDisplay]; + [mWindow setTitle:title]; + [mWindow enableSetNeedsDisplay]; + } else { + [mWindow setTitle:title]; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsCocoaWindow::Invalidate(const LayoutDeviceIntRect& aRect) +{ + if (mPopupContentView) { + return mPopupContentView->Invalidate(aRect); + } + + return NS_OK; +} + +// 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 +NS_IMETHODIMP +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] & NSResizableWindowMask) && [aWindow isZoomed]) + return nsSizeMode_Maximized; + return nsSizeMode_Normal; +} + +void +nsCocoaWindow::ReportMoveEvent() +{ + NS_OBJC_BEGIN_TRY_ABORT_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(); + + // Dispatch the move event to Gecko + NotifyWindowMoved(mBounds.x, mBounds.y); + + mInReportMoveEvent = false; + + NS_OBJC_END_TRY_ABORT_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); + } +} + +void +nsCocoaWindow::ReportSizeEvent() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + UpdateBounds(); + + if (mWidgetListener) { + LayoutDeviceIntRect innerBounds = GetClientBounds(); + mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetMenuBar(nsMenuBarX *aMenuBar) +{ + if (mMenuBar) + mMenuBar->SetParent(nullptr); + if (!mWindow) { + mMenuBar = nullptr; + return; + } + mMenuBar = 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. + id windowDelegate = [mWindow delegate]; + if (mMenuBar && + ((!gSomeMenuBarPainted && nsMenuUtilsX::GetHiddenWindowMenuBar() == mMenuBar) || + (windowDelegate && [windowDelegate toplevelActiveState]))) + mMenuBar->Paint(); +} + +NS_IMETHODIMP nsCocoaWindow::SetFocus(bool aState) +{ + if (!mWindow) + return NS_OK; + + if (mPopupContentView) { + mPopupContentView->SetFocus(aState); + } + else if (aState && ([mWindow isVisible] || [mWindow isMiniaturized])) { + if ([mWindow isMiniaturized]) { + [mWindow deminiaturize:nil]; + } + + [mWindow makeKeyAndOrderFront:nil]; + SendSetZLevelEvent(); + } + + return NS_OK; +} + +LayoutDeviceIntPoint nsCocoaWindow::WidgetToScreenOffset() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSRect rect = NSZeroRect; + LayoutDeviceIntRect r; + if (mWindow) { + rect = [mWindow contentRectForFrameRect:[mWindow frame]]; + } + r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(rect, BackingScaleFactor()); + + return r.TopLeft(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntPoint(0,0)); +} + +LayoutDeviceIntPoint nsCocoaWindow::GetClientOffset() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + LayoutDeviceIntRect clientRect = GetClientBounds(); + + return clientRect.TopLeft() - mBounds.TopLeft(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0)); +} + +LayoutDeviceIntSize +nsCocoaWindow::ClientToWindowSize(const LayoutDeviceIntSize& aClientSize) +{ + NS_OBJC_BEGIN_TRY_ABORT_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); + + NSRect inflatedRect = [mWindow frameRectForContentRect:rect]; + r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(inflatedRect, backingScale); + return r.Size(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntSize(0,0)); +} + +nsMenuBarX* nsCocoaWindow::GetMenuBar() +{ + return mMenuBar; +} + +void +nsCocoaWindow::CaptureRollupEvents(nsIRollupListener* aListener, + bool aDoCapture) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + gRollupListener = nullptr; + + 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()->RegisterForAllProcessMouseEvents(); + } + gRollupListener = aListener; + + // 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()->UnregisterAllProcessMouseEventHandlers(); + + // 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_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::GetAttention(int32_t aCycleCount) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + [NSApp requestUserAttention:NSInformationalRequest]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +bool +nsCocoaWindow::HasPendingInputEvent() +{ + return nsChildView::DoHasPendingInputEvent(); +} + +void +nsCocoaWindow::SetWindowShadowStyle(int32_t aStyle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + mShadowStyle = aStyle; + + // Shadowless windows are only supported on popups. + if (mWindowType == eWindowType_popup) + [mWindow setHasShadow:(aStyle != NS_STYLE_WINDOW_SHADOW_NONE)]; + + [mWindow setUseMenuStyle:(aStyle == NS_STYLE_WINDOW_SHADOW_MENU)]; + AdjustWindowShadow(); + SetWindowBackgroundBlur(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetShowsToolbarButton(bool aShow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mWindow) + [mWindow setShowsToolbarButton:aShow]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetShowsFullScreenButton(bool aShow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow respondsToSelector:@selector(toggleFullScreen:)] || + mSupportsNativeFullScreen == aShow) { + return; + } + + // If the window is currently in fullscreen mode, then we're going to + // transition out first, then set the collection behavior & toggle + // mSupportsNativeFullScreen, then transtion back into fullscreen mode. This + // prevents us from getting into a conflicting state with MakeFullScreen + // where mSupportsNativeFullScreen would lead us down the wrong path. + bool wasFullScreen = mInFullScreenMode; + + if (wasFullScreen) { + MakeFullScreen(false); + } + + NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior]; + if (aShow) { + newBehavior |= NSWindowCollectionBehaviorFullScreenPrimary; + } else { + newBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary; + } + [mWindow setCollectionBehavior:newBehavior]; + mSupportsNativeFullScreen = aShow; + + if (wasFullScreen) { + MakeFullScreen(true); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetWindowAnimationType(nsIWidget::WindowAnimationType aType) +{ + mAnimationType = aType; +} + +void +nsCocoaWindow::SetDrawsTitle(bool aDrawTitle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindow setWantsTitleDrawn:aDrawTitle]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsCocoaWindow::SetUseBrightTitlebarForeground(bool aBrightForeground) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindow setUseBrightTitlebarForeground:aBrightForeground]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::SetNonClientMargins(LayoutDeviceIntMargin &margins) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + SetDrawsInTitlebar(margins.top == 0); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void +nsCocoaWindow::SetWindowTitlebarColor(nscolor aColor, bool aActive) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // If they pass a color with a complete transparent alpha component, use the + // native titlebar appearance. + if (NS_GET_A(aColor) == 0) { + [mWindow setTitlebarColor:nil forActiveWindow:(BOOL)aActive]; + } else { + // Transform from sRGBA to monitor RGBA. This seems like it would make trying + // to match the system appearance lame, so probably we just shouldn't color + // correct chrome. + if (gfxPlatform::GetCMSMode() == eCMSMode_All) { + qcms_transform *transform = gfxPlatform::GetCMSRGBATransform(); + if (transform) { + uint8_t color[3]; + color[0] = NS_GET_R(aColor); + color[1] = NS_GET_G(aColor); + color[2] = NS_GET_B(aColor); + qcms_transform_data(transform, color, color, 1); + aColor = NS_RGB(color[0], color[1], color[2]); + } + } + + [mWindow setTitlebarColor:[NSColor colorWithDeviceRed:NS_GET_R(aColor)/255.0 + green:NS_GET_G(aColor)/255.0 + blue:NS_GET_B(aColor)/255.0 + alpha:NS_GET_A(aColor)/255.0] + forActiveWindow:(BOOL)aActive]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetDrawsInTitlebar(bool aState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mWindow) + [mWindow setDrawsContentsIntoWindowFrame:aState]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + AutoObserverNotifier notifier(aObserver, "mouseevent"); + if (mPopupContentView) + return mPopupContentView->SynthesizeNativeMouseEvent(aPoint, aNativeMessage, + aModifierFlags, nullptr); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void nsCocoaWindow::UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mPopupContentView) { + return mPopupContentView->UpdateThemeGeometries(aThemeGeometries); + } + + NS_OBJC_END_TRY_ABORT_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]; + } +} + +NS_IMETHODIMP_(void) +nsCocoaWindow::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mInputContext = aContext; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP_(bool) +nsCocoaWindow::ExecuteNativeKeyBinding(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) +{ + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + return keyBindings->Execute(aEvent, aCallback, aCallbackData); +} + +@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_ABORT_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_ABORT_BLOCK; +} + +- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + [super init]; + mGeckoWindow = geckoWind; + mToplevelActiveState = false; + mHasEverBeenZoomed = false; + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_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(); +} + +// 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)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_ABORT_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]; + + NS_OBJC_END_TRY_ABORT_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(); + } +} + +- (void)windowDidBecomeKey:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +- (void)windowDidResignKey:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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; +} + +- (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; +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +- (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 + +static float +GetDPI(NSWindow* aWindow) +{ + NSScreen* screen = [aWindow screen]; + if (!screen) + return 96.0f; + + CGDirectDisplayID displayID = + [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] intValue]; + CGFloat heightMM = ::CGDisplayScreenSize(displayID).height; + size_t heightPx = ::CGDisplayPixelsHigh(displayID); + if (heightMM < 1 || heightPx < 1) { + // Something extremely bogus is going on + return 96.0f; + } + + float dpi = heightPx / (heightMM / MM_PER_INCH_FLOAT); + + // Account for HiDPI mode where Cocoa's "points" do not correspond to real + // device pixels + CGFloat backingScale = GetBackingScaleFactor(aWindow); + + return dpi * backingScale; +} + +@interface NSView(FrameViewMethodSwizzling) +- (NSPoint)FrameView__closeButtonOrigin; +- (NSPoint)FrameView__fullScreenButtonOrigin; +@end + +@implementation NSView(FrameViewMethodSwizzling) + +- (NSPoint)FrameView__closeButtonOrigin +{ + NSPoint defaultPosition = [self FrameView__closeButtonOrigin]; + if ([[self window] isKindOfClass:[ToolbarWindow class]]) { + return [(ToolbarWindow*)[self window] windowButtonsPositionWithDefaultPosition:defaultPosition]; + } + return defaultPosition; +} + +- (NSPoint)FrameView__fullScreenButtonOrigin +{ + NSPoint defaultPosition = [self FrameView__fullScreenButtonOrigin]; + if ([[self window] isKindOfClass:[ToolbarWindow class]]) { + return [(ToolbarWindow*)[self window] fullScreenButtonPositionWithDefaultPosition:defaultPosition]; + } + return defaultPosition; +} + +@end + +static NSMutableSet *gSwizzledFrameViewClasses = nil; + +@interface NSWindow(PrivateSetNeedsDisplayInRectMethod) + - (void)_setNeedsDisplayInRect:(NSRect)aRect; +@end + +// This method is on NSThemeFrame starting with 10.10, but since NSThemeFrame +// is not a public class, we declare the method on NSView instead. We only have +// this declaration in order to avoid compiler warnings. +@interface NSView(PrivateAddKnownSubviewMethod) + - (void)_addKnownSubview:(NSView*)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView*)otherView; +@end + +// Available on 10.10 +@interface NSWindow(PrivateCornerMaskMethod) + - (id)_cornerMask; + - (void)_cornerMaskChanged; +@end + +#if !defined(MAC_OS_X_VERSION_10_10) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10 + +@interface NSImage(CapInsets) +- (void)setCapInsets:(NSEdgeInsets)capInsets; +@end + +#endif + +#if !defined(MAC_OS_X_VERSION_10_8) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 + +@interface NSImage(ImageCreationWithDrawingHandler) ++ (NSImage *)imageWithSize:(NSSize)size + flipped:(BOOL)drawingHandlerShouldBeCalledWithFlippedContext + drawingHandler:(BOOL (^)(NSRect dstRect))drawingHandler; +@end + +#endif + +@interface BaseWindow(Private) +- (void)removeTrackingArea; +- (void)cursorUpdated:(NSEvent*)aEvent; +- (void)updateContentViewSize; +- (void)reflowTitlebarElements; +@end + +@implementation BaseWindow + +- (id)_cornerMask +{ + if (!mUseMenuStyle) { + return [super _cornerMask]; + } + + 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; +} + +// The frame of a window is implemented using undocumented NSView subclasses. +// We offset the window buttons by overriding the methods _closeButtonOrigin +// and _fullScreenButtonOrigin 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_fullScreenButtonOrigin = + class_getMethodImplementation([NSView class], + @selector(FrameView__fullScreenButtonOrigin)); + + 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)); + } + IMP _fullScreenButtonOrigin = + class_getMethodImplementation(frameViewClass, + @selector(_fullScreenButtonOrigin)); + if (_fullScreenButtonOrigin && + _fullScreenButtonOrigin != our_fullScreenButtonOrigin) { + nsToolkit::SwizzleMethods(frameViewClass, @selector(_fullScreenButtonOrigin), + @selector(FrameView__fullScreenButtonOrigin)); + } + [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; + mActiveTitlebarColor = nil; + mInactiveTitlebarColor = nil; + mScheduledShadowInvalidation = NO; + mDisabledNeedsDisplay = NO; + mDPI = GetDPI(self); + mTrackingArea = nil; + mDirtyRect = NSZeroRect; + mBeingShown = NO; + mDrawTitle = NO; + mBrightTitlebarForeground = NO; + mUseMenuStyle = NO; + [self updateTrackingArea]; + + return self; +} + +- (void)setUseMenuStyle:(BOOL)aValue +{ + if (aValue != mUseMenuStyle) { + mUseMenuStyle = aValue; + if ([self respondsToSelector:@selector(_cornerMaskChanged)]) { + [self _cornerMaskChanged]; + } + } +} + +- (void)setBeingShown:(BOOL)aValue +{ + mBeingShown = aValue; +} + +- (BOOL)isBeingShown +{ + return mBeingShown; +} + +- (BOOL)isVisibleOrBeingShown +{ + return [super isVisible] || mBeingShown; +} + +- (void)disableSetNeedsDisplay +{ + mDisabledNeedsDisplay = YES; +} + +- (void)enableSetNeedsDisplay +{ + mDisabledNeedsDisplay = NO; +} + +- (void)dealloc +{ + [mActiveTitlebarColor release]; + [mInactiveTitlebarColor release]; + [self removeTrackingArea]; + ChildViewMouseTracker::OnDestroyWindow(self); + [super dealloc]; +} + +static const NSString* kStateTitleKey = @"title"; +static const NSString* kStateDrawsContentsIntoWindowFrameKey = @"drawsContentsIntoWindowFrame"; +static const NSString* kStateActiveTitlebarColorKey = @"activeTitlebarColor"; +static const NSString* kStateInactiveTitlebarColorKey = @"inactiveTitlebarColor"; +static const NSString* kStateShowsToolbarButton = @"showsToolbarButton"; +static const NSString* kStateCollectionBehavior = @"collectionBehavior"; + +- (void)importState:(NSDictionary*)aState +{ + [self setTitle:[aState objectForKey:kStateTitleKey]]; + [self setDrawsContentsIntoWindowFrame:[[aState objectForKey:kStateDrawsContentsIntoWindowFrameKey] boolValue]]; + [self setTitlebarColor:[aState objectForKey:kStateActiveTitlebarColorKey] forActiveWindow:YES]; + [self setTitlebarColor:[aState objectForKey:kStateInactiveTitlebarColorKey] forActiveWindow:NO]; + [self setShowsToolbarButton:[[aState objectForKey:kStateShowsToolbarButton] boolValue]]; + [self setCollectionBehavior:[[aState objectForKey:kStateCollectionBehavior] unsignedIntValue]]; +} + +- (NSMutableDictionary*)exportState +{ + NSMutableDictionary* state = [NSMutableDictionary dictionaryWithCapacity:10]; + [state setObject:[self title] forKey:kStateTitleKey]; + [state setObject:[NSNumber numberWithBool:[self drawsContentsIntoWindowFrame]] + forKey:kStateDrawsContentsIntoWindowFrameKey]; + NSColor* activeTitlebarColor = [self titlebarColorForActiveWindow:YES]; + if (activeTitlebarColor) { + [state setObject:activeTitlebarColor forKey:kStateActiveTitlebarColorKey]; + } + NSColor* inactiveTitlebarColor = [self titlebarColorForActiveWindow:NO]; + if (inactiveTitlebarColor) { + [state setObject:inactiveTitlebarColor forKey:kStateInactiveTitlebarColorKey]; + } + [state setObject:[NSNumber numberWithBool:[self showsToolbarButton]] + forKey:kStateShowsToolbarButton]; + [state setObject:[NSNumber numberWithUnsignedInt: [self collectionBehavior]] + forKey:kStateCollectionBehavior]; + return state; +} + +- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState +{ + bool changed = (aState != mDrawsIntoWindowFrame); + mDrawsIntoWindowFrame = aState; + if (changed) { + [self updateContentViewSize]; + [self reflowTitlebarElements]; + } +} + +- (BOOL)drawsContentsIntoWindowFrame +{ + return mDrawsIntoWindowFrame; +} + +- (void)setWantsTitleDrawn:(BOOL)aDrawTitle +{ + mDrawTitle = aDrawTitle; +} + +- (BOOL)wantsTitleDrawn +{ + return mDrawTitle; +} + +- (void)setUseBrightTitlebarForeground:(BOOL)aBrightForeground +{ + mBrightTitlebarForeground = aBrightForeground; + [[self standardWindowButton:NSWindowFullScreenButton] setNeedsDisplay:YES]; +} + +- (BOOL)useBrightTitlebarForeground +{ + return mBrightTitlebarForeground; +} + +// Pass nil here to get the default appearance. +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive +{ + [aColor retain]; + if (aActive) { + [mActiveTitlebarColor release]; + mActiveTitlebarColor = aColor; + } else { + [mInactiveTitlebarColor release]; + mInactiveTitlebarColor = aColor; + } +} + +- (NSColor*)titlebarColorForActiveWindow:(BOOL)aActive +{ + return aActive ? mActiveTitlebarColor : mInactiveTitlebarColor; +} + +- (void)deferredInvalidateShadow +{ + if (mScheduledShadowInvalidation || [self isOpaque] || ![self hasShadow]) + return; + + [self performSelector:@selector(invalidateShadow) withObject:nil afterDelay:0]; + mScheduledShadowInvalidation = YES; +} + +- (void)invalidateShadow +{ + [super invalidateShadow]; + mScheduledShadowInvalidation = NO; +} + +- (float)getDPI +{ + return mDPI; +} + +- (NSView*)trackingAreaView +{ + NSView* contentView = [self contentView]; + return [contentView superview] ? [contentView superview] : contentView; +} + +- (ChildView*)mainChildView +{ + NSView *contentView = [self contentView]; + // A PopupWindow's contentView is a ChildView object. + if ([contentView isKindOfClass:[ChildView class]]) { + return (ChildView*)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; +} + +- (void)updateContentViewSize +{ + NSRect rect = [self contentRectForFrameRect:[self frame]]; + [[self contentView] setFrameSize:rect.size]; +} + +// Possibly move the titlebar buttons. +- (void)reflowTitlebarElements +{ + NSView *frameView = [[self contentView] superview]; + if ([frameView respondsToSelector:@selector(_tileTitlebarAndRedisplay:)]) { + [frameView _tileTitlebarAndRedisplay:NO]; + } +} + +// Override methods that translate between content rect and frame rect. +- (NSRect)contentRectForFrameRect:(NSRect)aRect +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + return [super contentRectForFrameRect:aRect]; +} + +- (NSRect)contentRectForFrameRect:(NSRect)aRect styleMask:(NSUInteger)aMask +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + if ([super respondsToSelector:@selector(contentRectForFrameRect:styleMask:)]) { + return [super contentRectForFrameRect:aRect styleMask:aMask]; + } else { + return [NSWindow contentRectForFrameRect:aRect styleMask:aMask]; + } +} + +- (NSRect)frameRectForContentRect:(NSRect)aRect +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + return [super frameRectForContentRect:aRect]; +} + +- (NSRect)frameRectForContentRect:(NSRect)aRect styleMask:(NSUInteger)aMask +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + if ([super respondsToSelector:@selector(frameRectForContentRect:styleMask:)]) { + return [super frameRectForContentRect:aRect styleMask:aMask]; + } else { + return [NSWindow frameRectForContentRect:aRect styleMask:aMask]; + } +} + +- (void)setContentView:(NSView*)aView +{ + [super setContentView:aView]; + + // Now move the contentView to the bottommost layer so that it's guaranteed + // to be under the window buttons. + NSView* frameView = [aView superview]; + [aView removeFromSuperview]; + if ([frameView respondsToSelector:@selector(_addKnownSubview:positioned:relativeTo:)]) { + // 10.10 prints a warning when we call addSubview on the frame view, so we + // silence the warning by calling a private method instead. + [frameView _addKnownSubview:aView positioned:NSWindowBelow relativeTo:nil]; + } else { + [frameView addSubview:aView positioned:NSWindowBelow relativeTo:nil]; + } +} + +- (NSArray*)titlebarControls +{ + // Return all subviews of the frameView which are not the content view. + NSView* frameView = [[self contentView] superview]; + NSMutableArray* array = [[[frameView subviews] mutableCopy] autorelease]; + [array removeObject:[self contentView]]; + return array; +} + +- (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 +{ + 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; +} + +@end + +// This class allows us to exercise control over the window's title bar. This +// allows for a "unified toolbar" look without having to extend the content +// area into the title bar. It works like this: +// 1) We set the window's style to textured. +// 2) Because of this, the background color applies to the entire window, including +// the titlebar area. For normal textured windows, the default pattern is a +// "brushed metal" image on Tiger and a unified gradient on Leopard. +// 3) We set the background color to a custom NSColor subclass that knows how tall the window is. +// When -set is called on it, it sets a pattern (with a draw callback) as the fill. In that callback, +// it paints the the titlebar and background colors in the correct areas of the context it's given, +// which will fill the entire window (CG will tile it horizontally for us). +// 4) Whenever the window's main state changes and when [window display] is called, +// Cocoa redraws the titlebar using the patternDraw callback function. +// +// This class also provides us with a pill button to show/hide the toolbar up to 10.6. +// +// Drawing the unified gradient in the titlebar and the toolbar works like this: +// 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 NS_THEME_TOOLBAR. +// 3) nsChildView::UpdateThemeGeometries finds the toolbar frame's ToolbarWindow +// and passes the toolbar frame's height to setUnifiedToolbarHeight. +// 4) If the toolbar height has changed, a titlebar redraw is triggered and the +// upper part of the unified gradient is drawn in the titlebar. +// 5) The lower part of the unified gradient in the toolbar is drawn during +// normal window content painting in nsNativeThemeCocoa::DrawUnifiedToolbar. +// +// Whenever the unified gradient is drawn in the titlebar or the toolbar, both +// titlebar height and toolbar height must be known in order to construct the +// correct gradient. But you can only get from the toolbar frame +// to the containing window - the other direction doesn't work. That's why the +// toolbar height is cached in the ToolbarWindow but nsNativeThemeCocoa can simply +// query the window for its titlebar height when drawing the toolbar. +// +// Note that in drawsContentsIntoWindowFrame mode, titlebar drawing works in a +// completely different way: In that mode, the window's mainChildView will +// cover the titlebar completely and nothing that happens in the window +// background will reach the screen. +@implementation ToolbarWindow + +- (id)initWithContentRect:(NSRect)aContentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)aBufferingType defer:(BOOL)aFlag +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + aStyle = aStyle | NSTexturedBackgroundWindowMask; + if ((self = [super initWithContentRect:aContentRect styleMask:aStyle backing:aBufferingType defer:aFlag])) { + mColor = [[TitlebarAndBackgroundColor alloc] initWithWindow:self]; + // Bypass our guard method below. + [super setBackgroundColor:mColor]; + mBackgroundColor = [[NSColor whiteColor] retain]; + + mUnifiedToolbarHeight = 22.0f; + mWindowButtonsRect = NSZeroRect; + mFullScreenButtonRect = NSZeroRect; + + // setBottomCornerRounded: is a private API call, so we check to make sure + // we respond to it just in case. + if ([self respondsToSelector:@selector(setBottomCornerRounded:)]) + [self setBottomCornerRounded:YES]; + + [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge]; + [self setContentBorderThickness:0.0f forEdge:NSMaxYEdge]; + } + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [super setBackgroundColor:[NSColor whiteColor]]; + [mColor release]; + [mBackgroundColor release]; + [mTitlebarView release]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive +{ + [super setTitlebarColor:aColor forActiveWindow:aActive]; + [self setTitlebarNeedsDisplayInRect:[self titlebarRect]]; +} + +- (void)setBackgroundColor:(NSColor*)aColor +{ + [aColor retain]; + [mBackgroundColor release]; + mBackgroundColor = aColor; +} + +- (NSColor*)windowBackgroundColor +{ + return mBackgroundColor; +} + +- (void)setTemporaryBackgroundColor +{ + [super setBackgroundColor:[NSColor whiteColor]]; +} + +- (void)restoreBackgroundColor +{ + [super setBackgroundColor:mBackgroundColor]; +} + +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect +{ + [self setTitlebarNeedsDisplayInRect:aRect sync:NO]; +} + +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect sync:(BOOL)aSync +{ + NSRect titlebarRect = [self titlebarRect]; + NSRect rect = NSIntersectionRect(titlebarRect, aRect); + if (NSIsEmptyRect(rect)) + return; + + NSView* borderView = [[self contentView] superview]; + if (!borderView) + return; + + if (aSync) { + [borderView displayRect:rect]; + } else { + [borderView setNeedsDisplayInRect:rect]; + } +} + +- (NSRect)titlebarRect +{ + CGFloat titlebarHeight = [self titlebarHeight]; + return NSMakeRect(0, [self frame].size.height - titlebarHeight, + [self frame].size.width, titlebarHeight); +} + +// 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 in drawsContentsIntoWindowFrame mode. + NSRect frameRect = [self frame]; + NSRect originalContentRect = [NSWindow contentRectForFrameRect:frameRect styleMask:[self styleMask]]; + return NSMaxY(frameRect) - NSMaxY(originalContentRect); +} + +// Stores the complete height of titlebar + toolbar. +- (void)setUnifiedToolbarHeight:(CGFloat)aHeight +{ + if (aHeight == mUnifiedToolbarHeight) + return; + + mUnifiedToolbarHeight = aHeight; + + if (![self drawsContentsIntoWindowFrame]) { + // Redraw the title bar. If we're inside painting, we'll do it right now, + // otherwise we'll just invalidate it. + BOOL needSyncRedraw = ([NSView focusView] != nil); + [self setTitlebarNeedsDisplayInRect:[self titlebarRect] sync:needSyncRedraw]; + } +} + +// 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(); + } +} + +- (void)setWantsTitleDrawn:(BOOL)aDrawTitle +{ + [super setWantsTitleDrawn:aDrawTitle]; + [self setTitlebarNeedsDisplayInRect:[self titlebarRect]]; +} + +- (void)setSheetAttachmentPosition:(CGFloat)aY +{ + CGFloat topMargin = aY - [self titlebarHeight]; + [self setContentBorderThickness:topMargin forEdge:NSMaxYEdge]; +} + +- (void)placeWindowButtons:(NSRect)aRect +{ + if (!NSEqualRects(mWindowButtonsRect, aRect)) { + mWindowButtonsRect = aRect; + [self reflowTitlebarElements]; + } +} + +- (NSPoint)windowButtonsPositionWithDefaultPosition:(NSPoint)aDefaultPosition +{ + NSInteger styleMask = [self styleMask]; + if ([self drawsContentsIntoWindowFrame] && + !(styleMask & NSFullScreenWindowMask) && (styleMask & NSTitledWindowMask)) { + if (NSIsEmptyRect(mWindowButtonsRect)) { + // 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(0, [self frame].size.height); + } + return NSMakePoint(mWindowButtonsRect.origin.x, mWindowButtonsRect.origin.y); + } + return aDefaultPosition; +} + +- (void)placeFullScreenButton:(NSRect)aRect +{ + if (!NSEqualRects(mFullScreenButtonRect, aRect)) { + mFullScreenButtonRect = aRect; + [self reflowTitlebarElements]; + } +} + +- (NSPoint)fullScreenButtonPositionWithDefaultPosition:(NSPoint)aDefaultPosition +{ + if ([self drawsContentsIntoWindowFrame] && !NSIsEmptyRect(mFullScreenButtonRect)) { + return NSMakePoint(std::min(mFullScreenButtonRect.origin.x, aDefaultPosition.x), + std::min(mFullScreenButtonRect.origin.y, aDefaultPosition.y)); + } + return aDefaultPosition; +} + +// 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_ABORT_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_ABORT_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_ABORT_BLOCK_RETURN; + + NSWindow *nativeWindow = [self retain]; + BOOL retval = [super performKeyEquivalent:theEvent]; + [nativeWindow release]; + return retval; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +- (void)sendEvent:(NSEvent *)anEvent +{ + NSEventType type = [anEvent type]; + + switch (type) { + case NSScrollWheel: + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSRightMouseDown: + case NSRightMouseUp: + case NSOtherMouseDown: + case NSOtherMouseUp: + case NSMouseMoved: + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + { + // 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 + +// Custom NSColor subclass where most of the work takes place for drawing in +// the titlebar area. Not used in drawsContentsIntoWindowFrame mode. +@implementation TitlebarAndBackgroundColor + +- (id)initWithWindow:(ToolbarWindow*)aWindow +{ + if ((self = [super init])) { + mWindow = aWindow; // weak ref to avoid a cycle + } + return self; +} + +static void +DrawNativeTitlebar(CGContextRef aContext, CGRect aTitlebarRect, + CGFloat aUnifiedToolbarHeight, BOOL aIsMain) +{ + nsNativeThemeCocoa::DrawNativeTitlebar(aContext, aTitlebarRect, aUnifiedToolbarHeight, aIsMain, NO); + + // The call to CUIDraw doesn't draw the top pixel strip at some window widths. + // We don't want to have a flickering transparent line, so we overdraw it. + CGContextSetRGBFillColor(aContext, 0.95, 0.95, 0.95, 1); + CGContextFillRect(aContext, CGRectMake(0, CGRectGetMaxY(aTitlebarRect) - 1, + aTitlebarRect.size.width, 1)); +} + +// Pattern draw callback for standard titlebar gradients and solid titlebar colors +static void +TitlebarDrawCallback(void* aInfo, CGContextRef aContext) +{ + ToolbarWindow *window = (ToolbarWindow*)aInfo; + if (![window drawsContentsIntoWindowFrame]) { + NSRect titlebarRect = [window titlebarRect]; + BOOL isMain = [window isMainWindow]; + NSColor *titlebarColor = [window titlebarColorForActiveWindow:isMain]; + if (!titlebarColor) { + // If the titlebar color is nil, draw the default titlebar shading. + DrawNativeTitlebar(aContext, NSRectToCGRect(titlebarRect), + [window unifiedToolbarHeight], isMain); + } else { + // If the titlebar color is not nil, just set and draw it normally. + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:aContext flipped:NO]]; + [titlebarColor set]; + NSRectFill(titlebarRect); + [NSGraphicsContext restoreGraphicsState]; + } + } +} + +- (void)setFill +{ + float patternWidth = [mWindow frame].size.width; + + CGPatternCallbacks callbacks = {0, &TitlebarDrawCallback, NULL}; + CGPatternRef pattern = CGPatternCreate(mWindow, CGRectMake(0.0f, 0.0f, patternWidth, [mWindow frame].size.height), + CGAffineTransformIdentity, patternWidth, [mWindow frame].size.height, + kCGPatternTilingConstantSpacing, true, &callbacks); + + // Set the pattern as the fill, which is what we were asked to do. All our + // drawing will take place in the patternDraw callback. + CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL); + CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + CGContextSetFillColorSpace(context, patternSpace); + CGColorSpaceRelease(patternSpace); + CGFloat component = 1.0f; + CGContextSetFillPattern(context, pattern, &component); + CGPatternRelease(pattern); +} + +- (void)set +{ + [self setFill]; +} + +- (NSString*)colorSpaceName +{ + return NSDeviceRGBColorSpace; +} + +@end + +@implementation PopupWindow + +- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask + backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + mIsContextMenu = false; + return [super initWithContentRect:contentRect styleMask:styleMask + backing:bufferingType defer:deferCreation]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (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 NSScrollWheel: + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSRightMouseDown: + case NSRightMouseUp: + case NSOtherMouseDown: + case NSOtherMouseUp: + case NSMouseMoved: + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + { + // 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_ABORT_BLOCK_RETURN; + + if (![self isVisible]) + return NO; + return YES; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK_RETURN; + + NSWindow *nativeWindow = [self retain]; + BOOL retval = [super performKeyEquivalent:theEvent]; + [nativeWindow release]; + return retval; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +@end diff --git a/widget/cocoa/nsColorPicker.h b/widget/cocoa/nsColorPicker.h new file mode 100644 index 000000000..4b3e26218 --- /dev/null +++ b/widget/cocoa/nsColorPicker.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 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_IMETHOD Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle, + const nsAString& aInitialColor) override; + NS_IMETHOD Open(nsIColorPickerShownCallback* aCallback) override; + + // For NSColorPanelWrapper. + void Update(NSColor* aColor); + // Call this method if you are done with this input, but the color picker needs to + // stay open as it will be associated to another input + void DoneWithRetarget(); + // Same as DoneWithRetarget + clean the static instance of sColorPanelWrapper, + // as it is not needed anymore for now + void Done(); + +private: + ~nsColorPicker(); + + static NSColor* GetNSColorFromHexString(const nsAString& aColor); + static void GetHexStringFromNSColor(NSColor* aColor, nsAString& aResult); + + static NSColorPanelWrapper* sColorPanelWrapper; + + 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 000000000..263ea349b --- /dev/null +++ b/widget/cocoa/nsColorPicker.mm @@ -0,0 +1,188 @@ +/* -*- 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)retarget:(nsColorPicker*)aPicker; +- (void)colorChanged:(NSColorPanel*)aPanel; +@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 makeKeyAndOrderFront:nil]; +} + +- (void)colorChanged:(NSColorPanel*)aPanel +{ + mColorPicker->Update([mColorPanel color]); +} + +- (void)windowWillClose:(NSNotification*)aNotification +{ + mColorPicker->Done(); +} + +- (void)retarget:(nsColorPicker*)aPicker +{ + mColorPicker->DoneWithRetarget(); + mColorPicker = aPicker; +} + +- (void)dealloc +{ + [mColorPanel setTarget:nil]; + [mColorPanel setAction:nil]; + [mColorPanel setDelegate:nil]; + + mColorPanel = nil; + mColorPicker = nullptr; + + [super dealloc]; +} +@end + +NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker) + +NSColorPanelWrapper* nsColorPicker::sColorPanelWrapper = nullptr; + +nsColorPicker::~nsColorPicker() +{ +} + +NS_IMETHODIMP +nsColorPicker::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle, + const nsAString& aInitialColor) +{ + MOZ_ASSERT(NS_IsMainThread(), + "Color pickers can only be opened from main thread currently"); + mTitle = aTitle; + mColor = aInitialColor; + + if (sColorPanelWrapper) { + // Update current wrapper to target the new input instead + [sColorPanelWrapper retarget:this]; + } else { + // Create a brand new color panel wrapper + sColorPanelWrapper = [[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; + + [sColorPanelWrapper 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::DoneWithRetarget() +{ + mCallback->Done(EmptyString()); + mCallback = nullptr; + NS_RELEASE_THIS(); +} + +void +nsColorPicker::Done() +{ + [sColorPanelWrapper release]; + sColorPanelWrapper = nullptr; + DoneWithRetarget(); +} diff --git a/widget/cocoa/nsCursorManager.h b/widget/cocoa/nsCursorManager.h new file mode 100644 index 000000000..6dba8f903 --- /dev/null +++ b/widget/cocoa/nsCursorManager.h @@ -0,0 +1,65 @@ +/* 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 constant given as an argument. + Resources associated with the previous cursor are cleaned up. + @param aCursor the cursor to use +*/ +- (nsresult) setCursor: (nsCursor) aCursor; + +/*! @method setCursorWithImage:hotSpotX:hotSpotY: + @abstract Sets the current cursor to a custom image + @discussion Sets the current cursor to the cursor given by the aCursorImage argument. + Resources associated with the previous cursor are cleaned up. + @param aCursorImage the cursor image to use + @param aHotSpotX the x coordinate of the cursor's hotspot + @param aHotSpotY the y coordinate of the cursor's hotspot + @param scaleFactor the scale factor of the target display (2 for a retina display) + */ +- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY scaleFactor: (CGFloat) scaleFactor; + + +/*! @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 000000000..c4281a438 --- /dev/null +++ b/widget/cocoa/nsCursorManager.mm @@ -0,0 +1,308 @@ +/* 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 sCursorScaleFactor = 0.0f; +static imgIContainer *sCursorImgContainer = nullptr; +static const nsCursor sCustomCursor = 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_ABORT_BLOCK_NIL; + + if (!gInstance) { + gInstance = [[nsCursorManager alloc] init]; + } + return gInstance; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + ++ (void) dispose +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [gInstance release]; + gInstance = nil; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + ++ (nsMacCursor *) createCursor: (enum nsCursor) aCursor +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_NIL; +} + +- (id) init +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ((self = [super init])) { + mCursors = [[NSMutableDictionary alloc] initWithCapacity:25]; + } + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (nsresult) setCursor: (enum nsCursor) aCursor +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // Some plugins mess with our cursors and set a cursor that even + // [NSCursor currentCursor] doesn't know about. In case that happens, just + // reset the state. + [[NSCursor currentCursor] set]; + + nsCursor oldType = [mCurrentMacCursor type]; + if (oldType != aCursor) { + if (aCursor == eCursor_none) { + [NSCursor hide]; + } else if (oldType == eCursor_none) { + [NSCursor unhide]; + } + } + [self setMacCursor:[self getCursor:aCursor]]; + + // if a custom cursor was previously set, release sCursorImgContainer + if (oldType == sCustomCursor) { + NS_IF_RELEASE(sCursorImgContainer); + } + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +- (nsresult) setMacCursor: (nsMacCursor*) aMacCursor +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (mCurrentMacCursor != aMacCursor || ![mCurrentMacCursor isSet]) { + [aMacCursor retain]; + [mCurrentMacCursor unset]; + [aMacCursor set]; + [mCurrentMacCursor release]; + mCurrentMacCursor = aMacCursor; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY scaleFactor: (CGFloat) scaleFactor +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + // As the user moves the mouse, this gets called repeatedly with the same aCursorImage + if (sCursorImgContainer == aCursorImage && sCursorScaleFactor == scaleFactor && mCurrentMacCursor) { + [self setMacCursor:mCurrentMacCursor]; + return NS_OK; + } + + [[NSCursor currentCursor] set]; + int32_t width = 0, height = 0; + aCursorImage->GetWidth(&width); + aCursorImage->GetHeight(&height); + // prevent DoS attacks + if (width > 128 || height > 128) { + return NS_OK; + } + + NSImage *cursorImage; + nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(aCursorImage, imgIContainer::FRAME_FIRST, &cursorImage, scaleFactor); + if (NS_FAILED(rv) || !cursorImage) { + return NS_ERROR_FAILURE; + } + + // if the hotspot is nonsensical, make it 0,0 + aHotspotX = (aHotspotX > (uint32_t)width - 1) ? 0 : aHotspotX; + aHotspotY = (aHotspotY > (uint32_t)height - 1) ? 0 : aHotspotY; + + NSPoint hotSpot = ::NSMakePoint(aHotspotX, aHotspotY); + [self setMacCursor:[nsMacCursor cursorWithCursor:[[NSCursor alloc] initWithImage:cursorImage hotSpot:hotSpot] type:sCustomCursor]]; + [cursorImage release]; + + NS_IF_RELEASE(sCursorImgContainer); + sCursorImgContainer = aCursorImage; + sCursorScaleFactor = scaleFactor; + NS_ADDREF(sCursorImgContainer); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +- (nsMacCursor *) getCursor: (enum nsCursor) aCursor +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_NIL; +} + +- (void) dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mCurrentMacCursor unset]; + [mCurrentMacCursor release]; + [mCursors release]; + NS_IF_RELEASE(sCursorImgContainer); + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end diff --git a/widget/cocoa/nsDeviceContextSpecX.h b/widget/cocoa/nsDeviceContextSpecX.h new file mode 100644 index 000000000..2df52418a --- /dev/null +++ b/widget/cocoa/nsDeviceContextSpecX.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 <ApplicationServices/ApplicationServices.h> + +class nsDeviceContextSpecX : public nsIDeviceContextSpec +{ +public: + NS_DECL_ISUPPORTS + + nsDeviceContextSpecX(); + + NS_IMETHOD Init(nsIWidget *aWidget, nsIPrintSettings* aPS, bool aIsPrintPreview) override; + virtual already_AddRefed<PrintTarget> MakePrintTarget() final; + NS_IMETHOD BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, + int32_t aEndPage) override; + NS_IMETHOD EndDocument() override; + NS_IMETHOD BeginPage() override; + NS_IMETHOD EndPage() override; + + void GetPaperRect(double* aTop, double* aLeft, double* aBottom, double* aRight); + +protected: + virtual ~nsDeviceContextSpecX(); + +protected: + PMPrintSession mPrintSession; // printing context. + PMPageFormat mPageFormat; // page format. + PMPrintSettings mPrintSettings; // print settings. +}; + +#endif //nsDeviceContextSpecX_h_ diff --git a/widget/cocoa/nsDeviceContextSpecX.mm b/widget/cocoa/nsDeviceContextSpecX.mm new file mode 100644 index 000000000..d252adef6 --- /dev/null +++ b/widget/cocoa/nsDeviceContextSpecX.mm @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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" + +#include "mozilla/gfx/PrintTargetCG.h" +#include "mozilla/RefPtr.h" +#include "nsCRT.h" +#include <unistd.h> + +#include "nsQueryObject.h" +#include "nsIServiceManager.h" +#include "nsPrintSettingsX.h" + +// This must be the last include: +#include "nsObjCExceptions.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +nsDeviceContextSpecX::nsDeviceContextSpecX() +: mPrintSession(NULL) +, mPageFormat(kPMNoPageFormat) +, mPrintSettings(kPMNoPrintSettings) +{ +} + +nsDeviceContextSpecX::~nsDeviceContextSpecX() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mPrintSession) + ::PMRelease(mPrintSession); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMPL_ISUPPORTS(nsDeviceContextSpecX, nsIDeviceContextSpec) + +NS_IMETHODIMP nsDeviceContextSpecX::Init(nsIWidget *aWidget, + nsIPrintSettings* aPS, + bool aIsPrintPreview) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + RefPtr<nsPrintSettingsX> settings(do_QueryObject(aPS)); + if (!settings) + return NS_ERROR_NO_INTERFACE; + + mPrintSession = settings->GetPMPrintSession(); + ::PMRetain(mPrintSession); + mPageFormat = settings->GetPMPageFormat(); + mPrintSettings = settings->GetPMPrintSettings(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, + int32_t aEndPage) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!aTitle.IsEmpty()) { + CFStringRef cfString = + ::CFStringCreateWithCharacters(NULL, reinterpret_cast<const UniChar*>(aTitle.BeginReading()), + aTitle.Length()); + if (cfString) { + ::PMPrintSettingsSetJobName(mPrintSettings, cfString); + ::CFRelease(cfString); + } + } + + OSStatus status; + status = ::PMSetFirstPage(mPrintSettings, aStartPage, false); + NS_ASSERTION(status == noErr, "PMSetFirstPage failed"); + status = ::PMSetLastPage(mPrintSettings, aEndPage, false); + NS_ASSERTION(status == noErr, "PMSetLastPage failed"); + + status = ::PMSessionBeginCGDocumentNoDialog(mPrintSession, mPrintSettings, mPageFormat); + if (status != noErr) + return NS_ERROR_ABORT; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsDeviceContextSpecX::EndDocument() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + ::PMSessionEndDocumentNoDialog(mPrintSession); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsDeviceContextSpecX::BeginPage() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + PMSessionError(mPrintSession); + OSStatus status = ::PMSessionBeginPageNoDialog(mPrintSession, mPageFormat, NULL); + if (status != noErr) return NS_ERROR_ABORT; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsDeviceContextSpecX::EndPage() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + OSStatus status = ::PMSessionEndPageNoDialog(mPrintSession); + if (status != noErr) return NS_ERROR_ABORT; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void nsDeviceContextSpecX::GetPaperRect(double* aTop, double* aLeft, double* aBottom, double* aRight) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PMRect paperRect; + ::PMGetAdjustedPaperRect(mPageFormat, &paperRect); + + *aTop = paperRect.top, *aLeft = paperRect.left; + *aBottom = paperRect.bottom, *aRight = paperRect.right; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +already_AddRefed<PrintTarget> nsDeviceContextSpecX::MakePrintTarget() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + double top, left, bottom, right; + GetPaperRect(&top, &left, &bottom, &right); + const double width = right - left; + const double height = bottom - top; + IntSize size = IntSize::Floor(width, height); + + CGContextRef context; + ::PMSessionGetCGGraphicsContext(mPrintSession, &context); + + if (context) { + // Initially, origin is at bottom-left corner of the paper. + // Here, we translate it to top-left corner of the paper. + CGContextTranslateCTM(context, 0, height); + CGContextScaleCTM(context, 1.0, -1.0); + return PrintTargetCG::CreateOrNull(context, size); + } + + // Apparently we do need this branch - bug 368933. + return PrintTargetCG::CreateOrNull(size, SurfaceFormat::A8R8G8B8_UINT32); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; +} diff --git a/widget/cocoa/nsDragService.h b/widget/cocoa/nsDragService.h new file mode 100644 index 000000000..ea6702812 --- /dev/null +++ b/widget/cocoa/nsDragService.h @@ -0,0 +1,55 @@ +/* -*- 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 <Cocoa/Cocoa.h> + +extern NSString* const kWildcardPboardType; +extern NSString* const kCorePboardType_url; +extern NSString* const kCorePboardType_urld; +extern NSString* const kCorePboardType_urln; +extern NSString* const kCustomTypesPboardType; + +class nsDragService : public nsBaseDragService +{ +public: + nsDragService(); + + // nsBaseDragService + virtual nsresult InvokeDragSessionImpl(nsIArray* anArrayTransferables, + nsIScriptableRegion* aRegion, + uint32_t aActionType); + // nsIDragService + NS_IMETHOD EndDragSession(bool aDoneDrag); + + // nsIDragSession + NS_IMETHOD GetData(nsITransferable * aTransferable, uint32_t aItemIndex); + NS_IMETHOD IsDataFlavorSupported(const char *aDataFlavor, bool *_retval); + NS_IMETHOD GetNumDropItems(uint32_t * aNumItems); + +protected: + virtual ~nsDragService(); + +private: + + NSImage* ConstructDragImage(nsIDOMNode* aDOMNode, + mozilla::LayoutDeviceIntRect* aDragRect, + nsIScriptableRegion* aRegion); + bool IsValidType(NSString* availableType, bool allowFileURL); + NSString* GetStringForType(NSPasteboardItem* item, const NSString* type, + bool allowFileURL = false); + NSString* GetTitleForURL(NSPasteboardItem* item); + NSString* GetFilePath(NSPasteboardItem* item); + + nsCOMPtr<nsIArray> mDataItems; // only valid for a drag started within gecko + NSView* mNativeDragView; + NSEvent* mNativeDragEvent; +}; + +#endif // nsDragService_h_ diff --git a/widget/cocoa/nsDragService.mm b/widget/cocoa/nsDragService.mm new file mode 100644 index 000000000..b92db1b2a --- /dev/null +++ b/widget/cocoa/nsDragService.mm @@ -0,0 +1,669 @@ +/* -*- 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 "nsArrayUtils.h" +#include "nsDragService.h" +#include "nsArrayUtils.h" +#include "nsObjCExceptions.h" +#include "nsITransferable.h" +#include "nsString.h" +#include "nsClipboard.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsCOMPtr.h" +#include "nsPrimitiveHelpers.h" +#include "nsLinebreakConverter.h" +#include "nsIMacUtils.h" +#include "nsIDOMNode.h" +#include "nsRect.h" +#include "nsPoint.h" +#include "nsIIOService.h" +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsView.h" +#include "gfxContext.h" +#include "nsCocoaUtils.h" +#include "mozilla/gfx/2D.h" +#include "gfxPlatform.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +extern PRLogModuleInfo* sCocoaLog; + +extern void EnsureLogInitialized(); + +extern NSPasteboard* globalDragPboard; +extern NSView* gLastDragView; +extern NSEvent* gLastDragMouseDownEvent; +extern bool gUserCancelledDrag; + +// This global makes the transferable array available to Cocoa's promised +// file destination callback. +nsIArray *gDraggedTransferables = nullptr; + +NSString* const kWildcardPboardType = @"MozillaWildcard"; +NSString* const kCorePboardType_url = @"CorePasteboardFlavorType 0x75726C20"; // 'url ' url +NSString* const kCorePboardType_urld = @"CorePasteboardFlavorType 0x75726C64"; // 'urld' desc +NSString* const kCorePboardType_urln = @"CorePasteboardFlavorType 0x75726C6E"; // 'urln' title +NSString* const kUTTypeURLName = @"public.url-name"; +NSString* const kCustomTypesPboardType = @"org.mozilla.custom-clipdata"; + +nsDragService::nsDragService() +{ + mNativeDragView = nil; + mNativeDragEvent = nil; + + EnsureLogInitialized(); +} + +nsDragService::~nsDragService() +{ +} + +static nsresult SetUpDragClipboard(nsIArray* aTransferableArray) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!aTransferableArray) + return NS_ERROR_FAILURE; + + uint32_t count = 0; + aTransferableArray->GetLength(&count); + + NSPasteboard* dragPBoard = [NSPasteboard pasteboardWithName:NSDragPboard]; + + 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 + unsigned int typeCount = [pasteboardOutputDict count]; + NSMutableArray* types = [NSMutableArray arrayWithCapacity:typeCount + 1]; + [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:kWildcardPboardType]; // we don't increase the count for the loop below on purpose + [dragPBoard declareTypes:types owner:nil]; + for (unsigned int k = 0; k < typeCount; k++) { + NSString* currentKey = [types objectAtIndex:k]; + id currentValue = [pasteboardOutputDict valueForKey:currentKey]; + if (currentKey == NSStringPboardType || + currentKey == kCorePboardType_url || + currentKey == kCorePboardType_urld || + currentKey == kCorePboardType_urln) { + [dragPBoard setString:currentValue forType:currentKey]; + } + else if (currentKey == NSHTMLPboardType) { + [dragPBoard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue)) + forType:currentKey]; + } + else if (currentKey == NSTIFFPboardType || + currentKey == kCustomTypesPboardType) { + [dragPBoard setData:currentValue forType:currentKey]; + } + else if (currentKey == NSFilesPromisePboardType || + currentKey == NSFilenamesPboardType) { + [dragPBoard setPropertyList:currentValue forType:currentKey]; + } + } + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NSImage* +nsDragService::ConstructDragImage(nsIDOMNode* aDOMNode, + LayoutDeviceIntRect* aDragRect, + nsIScriptableRegion* aRegion) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(gLastDragView); + + RefPtr<SourceSurface> surface; + nsPresContext* pc; + nsresult rv = DrawDrag(aDOMNode, aRegion, mScreenPosition, + 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(mScreenPosition.x), + pc->CSSPixelsToDevPixels(mScreenPosition.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_ABORT_BLOCK_NIL; +} + +bool +nsDragService::IsValidType(NSString* availableType, bool allowFileURL) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // 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 + // kUTTypeURL, since it conforms to kUTTypeURL. + if (!allowFileURL && [availableType isEqualToString:(id)kUTTypeFileURL]) { + return false; + } + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK(false); +} + +NSString* +nsDragService::GetStringForType(NSPasteboardItem* item, const NSString* type, + bool allowFileURL) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSString* availableType = [item availableTypeFromArray:[NSArray arrayWithObjects:(id)type, nil]]; + if (availableType && IsValidType(availableType, allowFileURL)) { + return [item stringForType:(id)availableType]; + } + + return nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +NSString* +nsDragService::GetTitleForURL(NSPasteboardItem* item) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSString* name = GetStringForType(item, (const NSString*)kUTTypeURLName); + if (name) { + return name; + } + + NSString* filePath = GetFilePath(item); + if (filePath) { + return [filePath lastPathComponent]; + } + + return nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +NSString* +nsDragService::GetFilePath(NSPasteboardItem* item) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSString* urlString = GetStringForType(item, (const NSString*)kUTTypeFileURL, true); + if (urlString) { + NSURL* url = [NSURL URLWithString:urlString]; + if (url) { + return [url path]; + } + } + + return nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// We can only invoke NSView's 'dragImage:at:offset:event:pasteboard:source:slideBack:' from +// within NSView's 'mouseDown:' or 'mouseDragged:'. Luckily 'mouseDragged' is always on the +// stack when InvokeDragSession gets called. +nsresult +nsDragService::InvokeDragSessionImpl(nsIArray* aTransferableArray, + nsIScriptableRegion* aDragRgn, + uint32_t aActionType) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + mDataItems = aTransferableArray; + + // put data on the clipboard + if (NS_FAILED(SetUpDragClipboard(aTransferableArray))) + return NS_ERROR_FAILURE; + + CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(gLastDragView); + + LayoutDeviceIntRect dragRect(0, 0, 20, 20); + NSImage* image = ConstructDragImage(mSourceNode, &dragRect, aDragRgn); + 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 alloc] initWithSize:size]; + [image lockFocus]; + [[NSColor grayColor] set]; + NSBezierPath* path = [NSBezierPath bezierPath]; + [path setLineWidth:2.0]; + [path moveToPoint:NSMakePoint(0, 0)]; + [path lineToPoint:NSMakePoint(0, size.height)]; + [path lineToPoint:NSMakePoint(size.width, size.height)]; + [path lineToPoint:NSMakePoint(size.width, 0)]; + [path lineToPoint:NSMakePoint(0, 0)]; + [path stroke]; + [image unlockFocus]; + } + + LayoutDeviceIntPoint pt(dragRect.x, dragRect.YMost()); + NSPoint point = nsCocoaUtils::DevPixelsToCocoaPoints(pt, scaleFactor); + point.y = nsCocoaUtils::FlippedScreenY(point.y); + + point = nsCocoaUtils::ConvertPointFromScreen([gLastDragView window], point); + NSPoint localPoint = [gLastDragView convertPoint:point fromView:nil]; + + // Save the transferables away in case a promised file callback is invoked. + gDraggedTransferables = aTransferableArray; + + nsBaseDragService::StartDragSession(); + nsBaseDragService::OpenDragPopup(); + + // 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; + [mNativeDragView dragImage:image + at:localPoint + offset:NSZeroSize + event:mNativeDragEvent + pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard] + source:mNativeDragView + slideBack:YES]; + gUserCancelledDrag = false; + + if (mDoingDrag) + nsBaseDragService::EndDragSession(false); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!aTransferable) + return NS_ERROR_FAILURE; + + // get flavor list that includes all acceptable flavors (including ones obtained through conversion) + nsCOMPtr<nsIArray> flavorList; + nsresult rv = aTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavorList)); + if (NS_FAILED(rv)) + return NS_ERROR_FAILURE; + + uint32_t acceptableFlavorCount; + flavorList->GetLength(&acceptableFlavorCount); + + // 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 < acceptableFlavorCount; i++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, i); + if (!currentFlavor) + continue; + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); + + nsCOMPtr<nsISupports> dataSupports; + uint32_t dataSize = 0; + rv = currentTransferable->GetTransferData(flavorStr, getter_AddRefs(dataSupports), &dataSize); + if (NS_SUCCEEDED(rv)) { + aTransferable->SetTransferData(flavorStr, dataSupports, dataSize); + return NS_OK; // maybe try to fill in more types? Is there a point? + } + } + } + } + + // now check the actual clipboard for data + for (uint32_t i = 0; i < acceptableFlavorCount; i++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, i); + if (!currentFlavor) + continue; + + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); + + MOZ_LOG(sCocoaLog, LogLevel::Info, ("nsDragService::GetData: looking for clipboard data of type %s\n", flavorStr.get())); + + NSArray* droppedItems = [globalDragPboard pasteboardItems]; + if (!droppedItems) { + continue; + } + + uint32_t itemCount = [droppedItems count]; + if (aItemIndex >= itemCount) { + continue; + } + + NSPasteboardItem* item = [droppedItems objectAtIndex:aItemIndex]; + if (!item) { + continue; + } + + if (flavorStr.EqualsLiteral(kFileMime)) { + NSString* filePath = GetFilePath(item); + if (!filePath) + continue; + + 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 NS_ERROR_OUT_OF_MEMORY; + [filePath getCharacters:reinterpret_cast<unichar*>(clipboardDataPtr)]; + clipboardDataPtr[stringLength] = 0; // null terminate + + nsCOMPtr<nsIFile> file; + rv = NS_NewLocalFile(nsDependentString(clipboardDataPtr), true, getter_AddRefs(file)); + free(clipboardDataPtr); + if (NS_FAILED(rv)) + continue; + + aTransferable->SetTransferData(flavorStr, file, dataLength); + + break; + } + else if (flavorStr.EqualsLiteral(kCustomTypesMime)) { + NSString* availableType = [item availableTypeFromArray:[NSArray arrayWithObject:kCustomTypesPboardType]]; + if (!availableType || !IsValidType(availableType, false)) { + continue; + } + NSData *pasteboardData = [item dataForType:availableType]; + if (!pasteboardData) { + continue; + } + + unsigned int dataLength = [pasteboardData length]; + void* clipboardDataPtr = malloc(dataLength); + if (!clipboardDataPtr) { + return NS_ERROR_OUT_OF_MEMORY; + } + [pasteboardData getBytes:clipboardDataPtr]; + + nsCOMPtr<nsISupports> genericDataWrapper; + nsPrimitiveHelpers::CreatePrimitiveForData(flavorStr, clipboardDataPtr, dataLength, + getter_AddRefs(genericDataWrapper)); + + aTransferable->SetTransferData(flavorStr, genericDataWrapper, sizeof(nsIInputStream*)); + free(clipboardDataPtr); + break; + } + + NSString* pString = nil; + if (flavorStr.EqualsLiteral(kUnicodeMime)) { + pString = GetStringForType(item, (const NSString*)kUTTypeUTF8PlainText); + } else if (flavorStr.EqualsLiteral(kHTMLMime)) { + pString = GetStringForType(item, (const NSString*)kUTTypeHTML); + } else if (flavorStr.EqualsLiteral(kURLMime)) { + pString = GetStringForType(item, (const NSString*)kUTTypeURL); + if (pString) { + NSString* title = GetTitleForURL(item); + if (!title) { + title = pString; + } + pString = [NSString stringWithFormat:@"%@\n%@", pString, title]; + } + } else if (flavorStr.EqualsLiteral(kURLDataMime)) { + pString = GetStringForType(item, (const NSString*)kUTTypeURL); + } else if (flavorStr.EqualsLiteral(kURLDescriptionMime)) { + pString = GetTitleForURL(item); + } else if (flavorStr.EqualsLiteral(kRTFMime)) { + pString = GetStringForType(item, (const NSString*)kUTTypeRTF); + } + if (pString) { + NSData* stringData; + if (flavorStr.EqualsLiteral(kRTFMime)) { + 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]; + + // 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, genericDataWrapper, dataLength); + free(clipboardDataPtr); + break; + } + + // 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 (flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime)) { + + } + */ + } + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsDragService::IsDataFlavorSupported(const char *aDataFlavor, bool *_retval) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + *_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; + + nsCOMPtr<nsIArray> flavorList; + nsresult rv = currentTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavorList)); + if (NS_FAILED(rv)) + continue; + + uint32_t flavorCount; + flavorList->GetLength(&flavorCount); + for (uint32_t j = 0; j < flavorCount; j++) { + nsCOMPtr<nsISupportsCString> currentFlavor = do_QueryElementAt(flavorList, j); + if (!currentFlavor) + continue; + nsXPIDLCString flavorStr; + currentFlavor->ToString(getter_Copies(flavorStr)); + if (dataFlavor.Equals(flavorStr)) { + *_retval = true; + return NS_OK; + } + } + } + } + + const NSString* type = nil; + bool allowFileURL = false; + if (dataFlavor.EqualsLiteral(kFileMime)) { + type = (const NSString*)kUTTypeFileURL; + allowFileURL = true; + } else if (dataFlavor.EqualsLiteral(kUnicodeMime)) { + type = (const NSString*)kUTTypeUTF8PlainText; + } else if (dataFlavor.EqualsLiteral(kHTMLMime)) { + type = (const NSString*)kUTTypeHTML; + } else if (dataFlavor.EqualsLiteral(kURLMime) || + dataFlavor.EqualsLiteral(kURLDataMime)) { + type = (const NSString*)kUTTypeURL; + } else if (dataFlavor.EqualsLiteral(kURLDescriptionMime)) { + type = (const NSString*)kUTTypeURLName; + } else if (dataFlavor.EqualsLiteral(kRTFMime)) { + type = (const NSString*)kUTTypeRTF; + } else if (dataFlavor.EqualsLiteral(kCustomTypesMime)) { + type = (const NSString*)kCustomTypesPboardType; + } + + NSString* availableType = [globalDragPboard availableTypeFromArray:[NSArray arrayWithObjects:(id)type, nil]]; + if (availableType && IsValidType(availableType, allowFileURL)) { + *_retval = true; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsDragService::GetNumDropItems(uint32_t* aNumItems) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + *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_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsDragService::EndDragSession(bool aDoneDrag) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (mNativeDragView) { + [mNativeDragView release]; + mNativeDragView = nil; + } + if (mNativeDragEvent) { + [mNativeDragEvent release]; + mNativeDragEvent = nil; + } + + mUserCancelled = gUserCancelledDrag; + + nsresult rv = nsBaseDragService::EndDragSession(aDoneDrag); + mDataItems = nullptr; + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsFilePicker.h b/widget/cocoa/nsFilePicker.h new file mode 100644 index 000000000..1aeb22cc1 --- /dev/null +++ b/widget/cocoa/nsFilePicker.h @@ -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/. */ + +#ifndef nsFilePicker_h_ +#define nsFilePicker_h_ + +#include "nsBaseFilePicker.h" +#include "nsString.h" +#include "nsIFileChannel.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 Show(int16_t *_retval) 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; + + // 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(const nsString& inTitle, bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles); + int16_t GetLocalFolder(const nsString& inTitle, nsIFile** outFile); + int16_t PutLocalFile(const nsString& inTitle, const nsString& inDefaultName, nsIFile** outFile); + + void SetDialogTitle(const nsString& inTitle, id aDialog); + NSString *PanelDefaultDirectory(); + NSView* GetAccessoryView(); + + nsString mTitle; + nsCOMArray<nsIFile> mFiles; + nsString mDefault; + + 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 000000000..5213dee24 --- /dev/null +++ b/widget/cocoa/nsFilePicker.mm @@ -0,0 +1,676 @@ +/* -*- 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 "nsIComponentManager.h" +#include "nsIFile.h" +#include "nsILocalFileMac.h" +#include "nsIURL.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_ABORT_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_ABORT_BLOCK; +} + +nsFilePicker::nsFilePicker() +: mSelectedTypeIndex(0) +{ +} + +nsFilePicker::~nsFilePicker() +{ +} + +void +nsFilePicker::InitNative(nsIWidget *aParent, const nsAString& aTitle) +{ + mTitle = aTitle; +} + +NSView* nsFilePicker::GetAccessoryView() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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)) { + nsXPIDLString locaLabel; + bundle->GetStringFromName(u"formatLabel", getter_Copies(locaLabel)); + if (locaLabel) { + 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_ABORT_BLOCK_NIL; +} + +// Display the file dialog +NS_IMETHODIMP nsFilePicker::Show(int16_t *retval) +{ + NS_ENSURE_ARG_POINTER(retval); + + *retval = returnCancel; + + int16_t userClicksOK = returnCancel; + +// Random questions from DHH: +// +// Why do we pass mTitle, mDefault to the functions? Can GetLocalFile. PutLocalFile, +// and GetLocalFolder get called someplace else? It generates a bunch of warnings +// as it is right now. +// +// I think we could easily combine GetLocalFile and GetLocalFolder together, just +// setting panel pick options based on mMode. I didn't do it here b/c I wanted to +// make this look as much like Carbon nsFilePicker as possible. + + mFiles.Clear(); + nsCOMPtr<nsIFile> theFile; + + switch (mMode) + { + case modeOpen: + userClicksOK = GetLocalFiles(mTitle, false, mFiles); + break; + + case modeOpenMultiple: + userClicksOK = GetLocalFiles(mTitle, true, mFiles); + break; + + case modeSave: + userClicksOK = PutLocalFile(mTitle, mDefault, getter_AddRefs(theFile)); + break; + + case modeGetFolder: + userClicksOK = GetLocalFolder(mTitle, 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_ABORT_BLOCK_RETURN; + int32_t selectedItem = [mPopUpButton indexOfSelectedItem]; + if (selectedItem < 0) { + return; + } + + mFilePicker->SetFilterIndex(selectedItem); + UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList()); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(); +} +@end + +// Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in the dialog. +int16_t +nsFilePicker::GetLocalFiles(const nsString& inTitle, bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles) +{ + NS_OBJC_BEGIN_TRY_ABORT_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(inTitle, thePanel); + [thePanel setAllowsMultipleSelection:inAllowMultiple]; + [thePanel setCanSelectHiddenExtension:YES]; + [thePanel setCanChooseDirectories:NO]; + [thePanel setCanChooseFiles:YES]; + [thePanel setResolvesAliases:YES]; //this is default - probably doesn't need to be set + + // 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(EmptyString(), 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_ABORT_BLOCK_RETURN(0); +} + +// Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in the dialog. +int16_t +nsFilePicker::GetLocalFolder(const nsString& inTitle, nsIFile** outFile) +{ + NS_OBJC_BEGIN_TRY_ABORT_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(inTitle, thePanel); + [thePanel setAllowsMultipleSelection:NO]; //this is default -probably doesn't need to be set + [thePanel setCanSelectHiddenExtension:YES]; + [thePanel setCanChooseDirectories:YES]; + [thePanel setCanChooseFiles:NO]; + [thePanel setResolvesAliases:YES]; //this is default - probably doesn't need to be set + [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(EmptyString(), 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_ABORT_BLOCK_RETURN(0); +} + +// Returns |returnOK| if the user presses OK in the dialog. +int16_t +nsFilePicker::PutLocalFile(const nsString& inTitle, const nsString& inDefaultName, nsIFile** outFile) +{ + NS_OBJC_BEGIN_TRY_ABORT_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(inTitle, 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*)inDefaultName.get() length:inDefaultName.Length()]; + + // set up allowed types; this prevents the extension from being selected + // use the UTI for the file type to allow alternate extensions (e.g., jpg vs. jpeg) + NSString* extension = defaultFilename.pathExtension; + if (extension.length != 0) { + CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)extension, NULL); + + if (type) { + thePanel.allowedFileTypes = @[(NSString*)type]; + CFRelease(type); + } else { + // if there's no UTI for the file extension, use the extension itself. + thePanel.allowedFileTypes = @[extension]; + } + } + // Allow users to change the extension. + thePanel.allowsOtherFileTypes = YES; + + // 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(EmptyString(), 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_ABORT_BLOCK_RETURN(0); +} + +NSArray * +nsFilePicker::GetFilterList() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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(NS_LITERAL_STRING("*"))) { + 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_ABORT_BLOCK_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_ABORT_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_ABORT_BLOCK; +} + +// Converts path from an nsIFile into a NSString path +// If it fails, returns an empty string. +NSString * +nsFilePicker::PanelDefaultDirectory() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_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_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString) +{ + mDefault = 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(NS_LITERAL_STRING("*.app")); + } 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/nsIdleServiceX.h b/widget/cocoa/nsIdleServiceX.h new file mode 100644 index 000000000..f0b3d92ed --- /dev/null +++ b/widget/cocoa/nsIdleServiceX.h @@ -0,0 +1,33 @@ +/* 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 nsIdleServiceX_h_ +#define nsIdleServiceX_h_ + +#include "nsIdleService.h" + +class nsIdleServiceX : public nsIdleService +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + bool PollIdleTime(uint32_t* aIdleTime) override; + + static already_AddRefed<nsIdleServiceX> GetInstance() + { + RefPtr<nsIdleService> idleService = nsIdleService::GetInstance(); + if (!idleService) { + idleService = new nsIdleServiceX(); + } + + return idleService.forget().downcast<nsIdleServiceX>(); + } + +protected: + nsIdleServiceX() { } + virtual ~nsIdleServiceX() { } + bool UsePollMode() override; +}; + +#endif // nsIdleServiceX_h_ diff --git a/widget/cocoa/nsIdleServiceX.mm b/widget/cocoa/nsIdleServiceX.mm new file mode 100644 index 000000000..234a15414 --- /dev/null +++ b/widget/cocoa/nsIdleServiceX.mm @@ -0,0 +1,77 @@ +/* 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 "nsIdleServiceX.h" +#include "nsObjCExceptions.h" +#include "nsIServiceManager.h" +#import <Foundation/Foundation.h> + +NS_IMPL_ISUPPORTS_INHERITED0(nsIdleServiceX, nsIdleService) + +bool +nsIdleServiceX::PollIdleTime(uint32_t *aIdleTime) +{ + NS_OBJC_BEGIN_TRY_ABORT_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]; + 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_ABORT_BLOCK_RETURN(false); +} + +bool +nsIdleServiceX::UsePollMode() +{ + return true; +} + diff --git a/widget/cocoa/nsLookAndFeel.h b/widget/cocoa/nsLookAndFeel.h new file mode 100644 index 000000000..2ad31a2aa --- /dev/null +++ b/widget/cocoa/nsLookAndFeel.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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: public nsXPLookAndFeel { +public: + nsLookAndFeel(); + virtual ~nsLookAndFeel(); + + virtual nsresult NativeGetColor(ColorID aID, nscolor &aResult); + virtual nsresult GetIntImpl(IntID aID, int32_t &aResult); + virtual nsresult GetFloatImpl(FloatID aID, float &aResult); + virtual bool GetFontImpl(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle, + float aDevPixPerCSSPixel); + virtual char16_t GetPasswordCharacterImpl() + { + // unicode value for the bullet character, used for password textfields. + return 0x2022; + } + + static bool UseOverlayScrollbars(); + + virtual nsTArray<LookAndFeelInt> GetIntCacheImpl(); + virtual void SetIntCacheImpl(const nsTArray<LookAndFeelInt>& aLookAndFeelIntCache); + + virtual void RefreshImpl(); + +protected: + static bool SystemWantsOverlayScrollbars(); + static bool AllowOverlayScrollbarsOverlap(); + +private: + int32_t mUseOverlayScrollbars; + bool mUseOverlayScrollbarsCached; + + int32_t mAllowOverlayScrollbarsOverlap; + bool mAllowOverlayScrollbarsOverlapCached; +}; + +#endif // nsLookAndFeel_h_ diff --git a/widget/cocoa/nsLookAndFeel.mm b/widget/cocoa/nsLookAndFeel.mm new file mode 100644 index 000000000..cbee90f58 --- /dev/null +++ b/widget/cocoa/nsLookAndFeel.mm @@ -0,0 +1,581 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsLookAndFeel.h" +#include "nsCocoaFeatures.h" +#include "nsIServiceManager.h" +#include "nsNativeThemeColors.h" +#include "nsStyleConsts.h" +#include "nsCocoaFeatures.h" +#include "nsIContent.h" +#include "gfxFont.h" +#include "gfxFontConstants.h" +#include "gfxPlatformMac.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/widget/WidgetMessageUtils.h" + +#import <Cocoa/Cocoa.h> + +// This must be included last: +#include "nsObjCExceptions.h" + +enum { + mozNSScrollerStyleLegacy = 0, + mozNSScrollerStyleOverlay = 1 +}; +typedef NSInteger mozNSScrollerStyle; + +@interface NSScroller(AvailableSinceLion) ++ (mozNSScrollerStyle)preferredScrollerStyle; +@end + +nsLookAndFeel::nsLookAndFeel() + : nsXPLookAndFeel() + , mUseOverlayScrollbars(-1) + , mUseOverlayScrollbarsCached(false) + , mAllowOverlayScrollbarsOverlap(-1) + , mAllowOverlayScrollbarsOverlapCached(false) +{ +} + +nsLookAndFeel::~nsLookAndFeel() +{ +} + +static nscolor GetColorFromNSColor(NSColor* aColor) +{ + NSColor* deviceColor = [aColor colorUsingColorSpaceName:NSDeviceRGBColorSpace]; + return NS_RGB((unsigned int)([deviceColor redComponent] * 255.0), + (unsigned int)([deviceColor greenComponent] * 255.0), + (unsigned int)([deviceColor blueComponent] * 255.0)); +} + +static nscolor GetColorFromNSColorWithAlpha(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)); +} + +nsresult +nsLookAndFeel::NativeGetColor(ColorID aID, nscolor &aColor) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult res = NS_OK; + + switch (aID) { + case eColorID_WindowBackground: + aColor = NS_RGB(0xff,0xff,0xff); + break; + case eColorID_WindowForeground: + aColor = NS_RGB(0x00,0x00,0x00); + break; + case eColorID_WidgetBackground: + aColor = NS_RGB(0xdd,0xdd,0xdd); + break; + case eColorID_WidgetForeground: + aColor = NS_RGB(0x00,0x00,0x00); + break; + case eColorID_WidgetSelectBackground: + aColor = NS_RGB(0x80,0x80,0x80); + break; + case eColorID_WidgetSelectForeground: + aColor = NS_RGB(0x00,0x00,0x80); + break; + case eColorID_Widget3DHighlight: + aColor = NS_RGB(0xa0,0xa0,0xa0); + break; + case eColorID_Widget3DShadow: + aColor = NS_RGB(0x40,0x40,0x40); + break; + case eColorID_TextBackground: + aColor = NS_RGB(0xff,0xff,0xff); + break; + case eColorID_TextForeground: + aColor = NS_RGB(0x00,0x00,0x00); + break; + case eColorID_TextSelectBackground: + aColor = GetColorFromNSColor([NSColor selectedTextBackgroundColor]); + break; + case eColorID_highlight: // CSS2 color + aColor = GetColorFromNSColor([NSColor alternateSelectedControlColor]); + break; + case eColorID__moz_menuhover: + aColor = GetColorFromNSColor([NSColor alternateSelectedControlColor]); + break; + case eColorID_TextSelectForeground: + GetColor(eColorID_TextSelectBackground, aColor); + if (aColor == 0x000000) + aColor = NS_RGB(0xff,0xff,0xff); + else + aColor = NS_DONT_CHANGE_COLOR; + break; + case eColorID_highlighttext: // CSS2 color + case eColorID__moz_menuhovertext: + aColor = GetColorFromNSColor([NSColor alternateSelectedControlTextColor]); + break; + case eColorID_IMESelectedRawTextBackground: + case eColorID_IMESelectedConvertedTextBackground: + case eColorID_IMERawInputBackground: + case eColorID_IMEConvertedTextBackground: + aColor = NS_TRANSPARENT; + break; + case eColorID_IMESelectedRawTextForeground: + case eColorID_IMESelectedConvertedTextForeground: + case eColorID_IMERawInputForeground: + case eColorID_IMEConvertedTextForeground: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + break; + case eColorID_IMERawInputUnderline: + case eColorID_IMEConvertedTextUnderline: + aColor = NS_40PERCENT_FOREGROUND_COLOR; + break; + case eColorID_IMESelectedRawTextUnderline: + case eColorID_IMESelectedConvertedTextUnderline: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + break; + case eColorID_SpellCheckerUnderline: + aColor = NS_RGB(0xff, 0, 0); + 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 eColorID__moz_mac_buttonactivetext: + case eColorID__moz_mac_defaultbuttontext: + if (nsCocoaFeatures::OnYosemiteOrLater()) { + aColor = NS_RGB(0xFF,0xFF,0xFF); + break; + } + // Otherwise fall through and return the regular button text: + + case eColorID_buttontext: + case eColorID__moz_buttonhovertext: + aColor = GetColorFromNSColor([NSColor controlTextColor]); + break; + case eColorID_captiontext: + case eColorID_menutext: + case eColorID_infotext: + case eColorID__moz_menubartext: + aColor = GetColorFromNSColor([NSColor textColor]); + break; + case eColorID_windowtext: + aColor = GetColorFromNSColor([NSColor windowFrameTextColor]); + break; + case eColorID_activecaption: + aColor = GetColorFromNSColor([NSColor gridColor]); + break; + case eColorID_activeborder: + aColor = GetColorFromNSColor([NSColor keyboardFocusIndicatorColor]); + break; + case eColorID_appworkspace: + aColor = NS_RGB(0xFF,0xFF,0xFF); + break; + case eColorID_background: + aColor = NS_RGB(0x63,0x63,0xCE); + break; + case eColorID_buttonface: + case eColorID__moz_buttonhoverface: + aColor = NS_RGB(0xF0,0xF0,0xF0); + break; + case eColorID_buttonhighlight: + aColor = NS_RGB(0xFF,0xFF,0xFF); + break; + case eColorID_buttonshadow: + aColor = NS_RGB(0xDC,0xDC,0xDC); + break; + case eColorID_graytext: + aColor = GetColorFromNSColor([NSColor disabledControlTextColor]); + break; + case eColorID_inactiveborder: + aColor = GetColorFromNSColor([NSColor controlBackgroundColor]); + break; + case eColorID_inactivecaption: + aColor = GetColorFromNSColor([NSColor controlBackgroundColor]); + break; + case eColorID_inactivecaptiontext: + aColor = NS_RGB(0x45,0x45,0x45); + break; + case eColorID_scrollbar: + aColor = GetColorFromNSColor([NSColor scrollBarColor]); + break; + case eColorID_threeddarkshadow: + aColor = NS_RGB(0xDC,0xDC,0xDC); + break; + case eColorID_threedshadow: + aColor = NS_RGB(0xE0,0xE0,0xE0); + break; + case eColorID_threedface: + aColor = NS_RGB(0xF0,0xF0,0xF0); + break; + case eColorID_threedhighlight: + aColor = GetColorFromNSColor([NSColor highlightColor]); + break; + case eColorID_threedlightshadow: + aColor = NS_RGB(0xDA,0xDA,0xDA); + break; + case eColorID_menu: + aColor = GetColorFromNSColor([NSColor alternateSelectedControlTextColor]); + break; + case eColorID_infobackground: + aColor = NS_RGB(0xFF,0xFF,0xC7); + break; + case eColorID_windowframe: + aColor = GetColorFromNSColor([NSColor gridColor]); + break; + case eColorID_window: + case eColorID__moz_field: + case eColorID__moz_combobox: + aColor = NS_RGB(0xff,0xff,0xff); + break; + case eColorID__moz_fieldtext: + case eColorID__moz_comboboxtext: + aColor = GetColorFromNSColor([NSColor controlTextColor]); + break; + case eColorID__moz_dialog: + aColor = GetColorFromNSColor([NSColor controlHighlightColor]); + break; + case eColorID__moz_dialogtext: + case eColorID__moz_cellhighlighttext: + case eColorID__moz_html_cellhighlighttext: + aColor = GetColorFromNSColor([NSColor controlTextColor]); + break; + case eColorID__moz_dragtargetzone: + aColor = GetColorFromNSColor([NSColor selectedControlColor]); + break; + case eColorID__moz_mac_chrome_active: + case eColorID__moz_mac_chrome_inactive: { + int grey = NativeGreyColorAsInt(toolbarFillGrey, (aID == eColorID__moz_mac_chrome_active)); + aColor = NS_RGB(grey, grey, grey); + } + break; + case eColorID__moz_mac_focusring: + aColor = GetColorFromNSColorWithAlpha([NSColor keyboardFocusIndicatorColor], 0.48); + break; + case eColorID__moz_mac_menushadow: + aColor = NS_RGB(0xA3,0xA3,0xA3); + break; + case eColorID__moz_mac_menutextdisable: + aColor = NS_RGB(0x98,0x98,0x98); + break; + case eColorID__moz_mac_menutextselect: + aColor = GetColorFromNSColor([NSColor selectedMenuItemTextColor]); + break; + case eColorID__moz_mac_disabledtoolbartext: + aColor = GetColorFromNSColor([NSColor disabledControlTextColor]); + break; + case eColorID__moz_mac_menuselect: + aColor = GetColorFromNSColor([NSColor alternateSelectedControlColor]); + break; + case eColorID__moz_buttondefault: + aColor = NS_RGB(0xDC,0xDC,0xDC); + break; + case eColorID__moz_cellhighlight: + case eColorID__moz_html_cellhighlight: + case eColorID__moz_mac_secondaryhighlight: + // For inactive list selection + aColor = GetColorFromNSColor([NSColor secondarySelectedControlColor]); + break; + case eColorID__moz_eventreerow: + // Background color of even list rows. + aColor = GetColorFromNSColor([[NSColor controlAlternatingRowBackgroundColors] objectAtIndex:0]); + break; + case eColorID__moz_oddtreerow: + // Background color of odd list rows. + aColor = GetColorFromNSColor([[NSColor controlAlternatingRowBackgroundColors] objectAtIndex:1]); + break; + case eColorID__moz_nativehyperlinktext: + // There appears to be no available system defined color. HARDCODING to the appropriate color. + aColor = NS_RGB(0x14,0x4F,0xAE); + break; + default: + NS_WARNING("Someone asked nsILookAndFeel for a color I don't know about"); + aColor = NS_RGB(0xff,0xff,0xff); + res = NS_ERROR_FAILURE; + break; + } + + return res; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult +nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult res = nsXPLookAndFeel::GetIntImpl(aID, aResult); + if (NS_SUCCEEDED(res)) + return res; + res = NS_OK; + + switch (aID) { + case eIntID_CaretBlinkTime: + aResult = 567; + break; + case eIntID_CaretWidth: + aResult = 1; + break; + case eIntID_ShowCaretDuringSelection: + aResult = 0; + break; + case eIntID_SelectTextfieldsOnKeyFocus: + // Select textfield content when focused by kbd + // used by EventStateManager::sTextfieldSelectModel + aResult = 1; + break; + case eIntID_SubmenuDelay: + aResult = 200; + break; + case eIntID_TooltipDelay: + aResult = 500; + break; + case eIntID_MenusCanOverlapOSBar: + // xul popups are not allowed to overlap the menubar. + aResult = 0; + break; + case eIntID_SkipNavigatingDisabledMenuItem: + aResult = 1; + break; + case eIntID_DragThresholdX: + case eIntID_DragThresholdY: + aResult = 4; + break; + case eIntID_ScrollArrowStyle: + aResult = eScrollArrow_None; + break; + case eIntID_ScrollSliderStyle: + aResult = eScrollThumbStyle_Proportional; + break; + case eIntID_UseOverlayScrollbars: + if (!mUseOverlayScrollbarsCached) { + mUseOverlayScrollbars = SystemWantsOverlayScrollbars() ? 1 : 0; + mUseOverlayScrollbarsCached = true; + } + aResult = mUseOverlayScrollbars; + break; + case eIntID_AllowOverlayScrollbarsOverlap: + if (!mAllowOverlayScrollbarsOverlapCached) { + mAllowOverlayScrollbarsOverlap = AllowOverlayScrollbarsOverlap() ? 1 : 0; + mAllowOverlayScrollbarsOverlapCached = true; + } + aResult = mAllowOverlayScrollbarsOverlap; + break; + case eIntID_ScrollbarDisplayOnMouseMove: + aResult = 0; + break; + case eIntID_ScrollbarFadeBeginDelay: + aResult = 450; + break; + case eIntID_ScrollbarFadeDuration: + aResult = 200; + break; + case eIntID_TreeOpenDelay: + aResult = 1000; + break; + case eIntID_TreeCloseDelay: + aResult = 1000; + break; + case eIntID_TreeLazyScrollDelay: + aResult = 150; + break; + case eIntID_TreeScrollDelay: + aResult = 100; + break; + case eIntID_TreeScrollLinesMax: + aResult = 3; + break; + case eIntID_DWMCompositor: + case eIntID_WindowsClassic: + case eIntID_WindowsDefaultTheme: + case eIntID_TouchEnabled: + case eIntID_WindowsThemeIdentifier: + case eIntID_OperatingSystemVersionIdentifier: + aResult = 0; + res = NS_ERROR_NOT_IMPLEMENTED; + break; + case eIntID_MacGraphiteTheme: + aResult = [NSColor currentControlTint] == NSGraphiteControlTint; + break; + case eIntID_MacYosemiteTheme: + aResult = nsCocoaFeatures::OnYosemiteOrLater(); + break; + case eIntID_AlertNotificationOrigin: + aResult = NS_ALERT_TOP; + break; + case eIntID_TabFocusModel: + aResult = [NSApp isFullKeyboardAccessEnabled] ? + nsIContent::eTabFocus_any : nsIContent::eTabFocus_textControlsMask; + break; + case eIntID_ScrollToClick: + { + aResult = [[NSUserDefaults standardUserDefaults] boolForKey:@"AppleScrollerPagingBehavior"]; + } + break; + case eIntID_ChosenMenuItemsShouldBlink: + aResult = 1; + break; + case eIntID_IMERawInputUnderlineStyle: + case eIntID_IMEConvertedTextUnderlineStyle: + case eIntID_IMESelectedRawTextUnderlineStyle: + case eIntID_IMESelectedConvertedTextUnderline: + aResult = NS_STYLE_TEXT_DECORATION_STYLE_SOLID; + break; + case eIntID_SpellCheckerUnderlineStyle: + aResult = NS_STYLE_TEXT_DECORATION_STYLE_DOTTED; + break; + case eIntID_ScrollbarButtonAutoRepeatBehavior: + aResult = 0; + break; + case eIntID_SwipeAnimationEnabled: + aResult = 0; + if ([NSEvent respondsToSelector:@selector( + isSwipeTrackingFromScrollEventsEnabled)]) { + aResult = [NSEvent isSwipeTrackingFromScrollEventsEnabled] ? 1 : 0; + } + break; + case eIntID_ColorPickerAvailable: + aResult = 1; + break; + case eIntID_ContextMenuOffsetVertical: + aResult = -6; + break; + case eIntID_ContextMenuOffsetHorizontal: + aResult = 1; + break; + default: + aResult = 0; + res = NS_ERROR_FAILURE; + } + return res; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult +nsLookAndFeel::GetFloatImpl(FloatID aID, float &aResult) +{ + nsresult res = nsXPLookAndFeel::GetFloatImpl(aID, aResult); + if (NS_SUCCEEDED(res)) + return res; + res = NS_OK; + + switch (aID) { + case eFloatID_IMEUnderlineRelativeSize: + aResult = 2.0f; + break; + case eFloatID_SpellCheckerUnderlineRelativeSize: + aResult = 2.0f; + break; + default: + aResult = -1.0; + res = NS_ERROR_FAILURE; + } + + return res; +} + +bool nsLookAndFeel::UseOverlayScrollbars() +{ + return GetInt(eIntID_UseOverlayScrollbars) != 0; +} + +bool nsLookAndFeel::SystemWantsOverlayScrollbars() +{ + return ([NSScroller respondsToSelector:@selector(preferredScrollerStyle)] && + [NSScroller preferredScrollerStyle] == mozNSScrollerStyleOverlay); +} + +bool nsLookAndFeel::AllowOverlayScrollbarsOverlap() +{ + return (UseOverlayScrollbars()); +} + +bool +nsLookAndFeel::GetFontImpl(FontID aID, nsString &aFontName, + gfxFontStyle &aFontStyle, + float aDevPixPerCSSPixel) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + // hack for now + if (aID == eFont_Window || aID == eFont_Document) { + aFontStyle.style = NS_FONT_STYLE_NORMAL; + aFontStyle.weight = NS_FONT_WEIGHT_NORMAL; + aFontStyle.stretch = NS_FONT_STRETCH_NORMAL; + aFontStyle.size = 14 * aDevPixPerCSSPixel; + aFontStyle.systemFont = true; + + aFontName.AssignLiteral("sans-serif"); + return true; + } + + gfxPlatformMac::LookupSystemFont(aID, aFontName, aFontStyle, + aDevPixPerCSSPixel); + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +nsTArray<LookAndFeelInt> +nsLookAndFeel::GetIntCacheImpl() +{ + nsTArray<LookAndFeelInt> lookAndFeelIntCache = + nsXPLookAndFeel::GetIntCacheImpl(); + + LookAndFeelInt useOverlayScrollbars; + useOverlayScrollbars.id = eIntID_UseOverlayScrollbars; + useOverlayScrollbars.value = GetInt(eIntID_UseOverlayScrollbars); + lookAndFeelIntCache.AppendElement(useOverlayScrollbars); + + LookAndFeelInt allowOverlayScrollbarsOverlap; + allowOverlayScrollbarsOverlap.id = eIntID_AllowOverlayScrollbarsOverlap; + allowOverlayScrollbarsOverlap.value = GetInt(eIntID_AllowOverlayScrollbarsOverlap); + lookAndFeelIntCache.AppendElement(allowOverlayScrollbarsOverlap); + + return lookAndFeelIntCache; +} + +void +nsLookAndFeel::SetIntCacheImpl(const nsTArray<LookAndFeelInt>& aLookAndFeelIntCache) +{ + for (auto entry : aLookAndFeelIntCache) { + switch(entry.id) { + case eIntID_UseOverlayScrollbars: + mUseOverlayScrollbars = entry.value; + mUseOverlayScrollbarsCached = true; + break; + case eIntID_AllowOverlayScrollbarsOverlap: + mAllowOverlayScrollbarsOverlap = entry.value; + mAllowOverlayScrollbarsOverlapCached = true; + break; + } + } +} + +void +nsLookAndFeel::RefreshImpl() +{ + // We should only clear the cache if we're in the main browser process. + // Otherwise, we should wait for the parent to inform us of new values + // to cache via LookAndFeel::SetIntCache. + if (XRE_IsParentProcess()) { + mUseOverlayScrollbarsCached = false; + mAllowOverlayScrollbarsOverlapCached = false; + } +} diff --git a/widget/cocoa/nsMacCursor.h b/widget/cocoa/nsMacCursor.h new file mode 100644 index 000000000..cf9c84c7e --- /dev/null +++ b/widget/cocoa/nsMacCursor.h @@ -0,0 +1,105 @@ +/* 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 000000000..4fcdfd3e5 --- /dev/null +++ b/widget/cocoa/nsMacCursor.mm @@ -0,0 +1,382 @@ +/* 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_ABORT_BLOCK_NIL; + + return [[[nsCocoaCursor alloc] initWithCursor:aCursor type:aType] autorelease]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + ++ (nsMacCursor *) cursorWithImageNamed: (NSString *) aCursorImage hotSpot: (NSPoint) aPoint type: (nsCursor) aType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [[[nsCocoaCursor alloc] initWithImageNamed:aCursorImage hotSpot:aPoint type:aType] autorelease]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + ++ (nsMacCursor *) cursorWithFrames: (NSArray *) aCursorFrames type: (nsCursor) aType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [[[nsCocoaCursor alloc] initWithFrames:aCursorFrames type:aType] autorelease]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + ++ (NSCursor *) cocoaCursorWithImageNamed: (NSString *) imageName hotSpot: (NSPoint) aPoint +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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(NS_LITERAL_CSTRING("res")); + resDir->AppendNative(NS_LITERAL_CSTRING("cursors")); + + 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_ABORT_BLOCK_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_ABORT_BLOCK; + + if (!mTimer) { + mTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25 + target:self + selector:@selector(advanceAnimatedCursor:) + userInfo:nil + repeats:YES] retain]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void) destroyTimer +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mTimer) { + [mTimer invalidate]; + [mTimer release]; + mTimer = nil; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void) advanceAnimatedCursor: (NSTimer *) aTimer +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if ([aTimer isValid]) { + [self setFrame:[self getNextCursorFrame]]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void) setFrame: (int) aFrameIndex +{ + // subclasses need to do something useful here +} + +- (nsCursor) type { + return mType; +} + +- (void) dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [self destroyTimer]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end + +@implementation nsCocoaCursor + +- (id) initWithFrames: (NSArray *) aCursorFrames type: (nsCursor) aType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_NIL; +} + +- (id) initWithCursor: (NSCursor *) aCursor type: (nsCursor) aType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSArray *frame = [NSArray arrayWithObjects:aCursor, nil]; + return [self initWithFrames:frame type:aType]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id) initWithImageNamed: (NSString *) aCursorImage hotSpot: (NSPoint) aPoint type: (nsCursor) aType +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [self initWithCursor:[nsMacCursor cocoaCursorWithImageNamed:aCursorImage hotSpot:aPoint] type:aType]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL) isSet +{ + return [NSCursor currentCursor] == mLastSetCocoaCursor; +} + +- (void) setFrame: (int) aFrameIndex +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NSCursor* newCursor = [mFrames objectAtIndex:aFrameIndex]; + [newCursor set]; + mLastSetCocoaCursor = newCursor; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (int) numFrames +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return [mFrames count]; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +- (NSString *) description +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [mFrames description]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void) dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mFrames release]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end diff --git a/widget/cocoa/nsMacDockSupport.h b/widget/cocoa/nsMacDockSupport.h new file mode 100644 index 000000000..a638b89e0 --- /dev/null +++ b/widget/cocoa/nsMacDockSupport.h @@ -0,0 +1,41 @@ +/* -*- 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 "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsNativeThemeCocoa.h" + +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; + + NSImage *mAppIcon, *mProgressBackground; + + HIRect mProgressBounds; + nsTaskbarProgressState mProgressState; + double mProgressFraction; + nsCOMPtr<nsITimer> mProgressTimer; + RefPtr<nsNativeThemeCocoa> mTheme; + + static void RedrawIconCallback(nsITimer* aTimer, void* aClosure); + + bool InitProgress(); + nsresult RedrawIcon(); +}; diff --git a/widget/cocoa/nsMacDockSupport.mm b/widget/cocoa/nsMacDockSupport.mm new file mode 100644 index 000000000..56b37822b --- /dev/null +++ b/widget/cocoa/nsMacDockSupport.mm @@ -0,0 +1,174 @@ +/* -*- 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 "nsMacDockSupport.h" +#include "nsObjCExceptions.h" + +NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress) + +nsMacDockSupport::nsMacDockSupport() +: mAppIcon(nil) +, mProgressBackground(nil) +, mProgressState(STATE_NO_PROGRESS) +, mProgressFraction(0.0) +{ + mProgressTimer = do_CreateInstance(NS_TIMER_CONTRACTID); +} + +nsMacDockSupport::~nsMacDockSupport() +{ + if (mAppIcon) { + [mAppIcon release]; + mAppIcon = nil; + } + if (mProgressBackground) { + [mProgressBackground release]; + mProgressBackground = nil; + } + if (mProgressTimer) { + mProgressTimer->Cancel(); + mProgressTimer = nullptr; + } +} + +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_ABORT_BLOCK_NSRESULT; + + [[NSApplication sharedApplication] activateIgnoringOtherApps:aIgnoreOtherApplications]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +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; + } + + if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) { + int perSecond = 8; // Empirically determined, see bug 848792 + mProgressTimer->InitWithFuncCallback(RedrawIconCallback, this, 1000 / perSecond, + nsITimer::TYPE_REPEATING_SLACK); + return NS_OK; + } else { + mProgressTimer->Cancel(); + return RedrawIcon(); + } +} + +// static +void nsMacDockSupport::RedrawIconCallback(nsITimer* aTimer, void* aClosure) +{ + static_cast<nsMacDockSupport*>(aClosure)->RedrawIcon(); +} + +// Return whether to draw progress +bool nsMacDockSupport::InitProgress() +{ + if (mProgressState != STATE_NORMAL && mProgressState != STATE_INDETERMINATE) { + return false; + } + + if (!mAppIcon) { + mProgressTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mAppIcon = [[NSImage imageNamed:@"NSApplicationIcon"] retain]; + mProgressBackground = [mAppIcon copyWithZone:nil]; + mTheme = new nsNativeThemeCocoa(); + + NSSize sz = [mProgressBackground size]; + mProgressBounds = CGRectMake(sz.width * 1/32, sz.height * 3/32, + sz.width * 30/32, sz.height * 4/32); + [mProgressBackground lockFocus]; + [[NSColor whiteColor] set]; + NSRectFill(NSRectFromCGRect(mProgressBounds)); + [mProgressBackground unlockFocus]; + } + return true; +} + +nsresult +nsMacDockSupport::RedrawIcon() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (InitProgress()) { + // TODO: - Implement ERROR and PAUSED states? + NSImage *icon = [mProgressBackground copyWithZone:nil]; + bool isIndeterminate = (mProgressState != STATE_NORMAL); + + [icon lockFocus]; + CGContextRef ctx = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + mTheme->DrawProgress(ctx, mProgressBounds, isIndeterminate, + true, mProgressFraction, 1.0, NULL); + [icon unlockFocus]; + [NSApp setApplicationIconImage:icon]; + [icon release]; + } else { + [NSApp setApplicationIconImage:mAppIcon]; + } + + return NS_OK; + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsMacWebAppUtils.h b/widget/cocoa/nsMacWebAppUtils.h new file mode 100644 index 000000000..98ef23561 --- /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 000000000..1b98cef7c --- /dev/null +++ b/widget/cocoa/nsMacWebAppUtils.mm @@ -0,0 +1,82 @@ +/* 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_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsMacWebAppUtils::LaunchAppWithIdentifier(const nsAString& bundleIdentifier) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsMacWebAppUtils::TrashApp(const nsAString& path, nsITrashAppCallback* aCallback) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h new file mode 100644 index 000000000..7cbb8ce62 --- /dev/null +++ b/widget/cocoa/nsMenuBarX.h @@ -0,0 +1,128 @@ +/* -*- 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 "nsMenuBaseX.h" +#include "nsMenuGroupOwnerX.h" +#include "nsChangeObserver.h" +#include "nsINativeMenuService.h" +#include "nsString.h" + +class nsMenuX; +class nsIWidget; +class nsIContent; + +// The native menu service for creating native menu bars. +class nsNativeMenuServiceX : public nsINativeMenuService +{ +public: + NS_DECL_ISUPPORTS + + nsNativeMenuServiceX() {} + + NS_IMETHOD CreateNativeMenuBar(nsIWidget* aParent, nsIContent* aMenuBarNode) override; + +protected: + virtual ~nsNativeMenuServiceX() {} +}; + +// Objective-C class used to allow us to intervene with keyboard event handling. +// We allow mouse actions to work normally. +@interface GeckoNSMenu : NSMenu +{ +} +@end + +// Objective-C class used as action target for menu items +@interface NativeMenuItemTarget : NSObject +{ +} +-(IBAction)menuItemHit:(id)sender; +@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)sender; +@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 *)newItem; +- (NSMenuItem *)addItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv; +- (void)insertItem:(NSMenuItem *)newItem atIndex:(NSInteger)index; +- (NSMenuItem *)insertItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv atIndex:(NSInteger)index; +- (void) _overrideClassOfMenuItem:(NSMenuItem *)menuItem; +@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 nsMenuGroupOwnerX, public nsChangeObserver +{ +public: + nsMenuBarX(); + virtual ~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. + nsCOMPtr<nsIContent> mAboutItemContent; + nsCOMPtr<nsIContent> mPrefItemContent; + nsCOMPtr<nsIContent> mQuitItemContent; + + // nsChangeObserver + NS_DECL_CHANGEOBSERVER + + // nsMenuObjectX + void* NativeData() override {return (void*)mNativeMenu;} + nsMenuObjectTypeX MenuObjectType() override {return eMenuBarObjectType;} + + // nsMenuBarX + nsresult Create(nsIWidget* aParent, nsIContent* aContent); + void SetParent(nsIWidget* aParent); + uint32_t GetMenuCount(); + bool MenuContainsAppMenu(); + nsMenuX* GetMenuAt(uint32_t aIndex); + nsMenuX* GetXULHelpMenu(); + void SetSystemHelpMenu(); + nsresult Paint(); + void ForceUpdateNativeMenuAt(const nsAString& indexString); + void ForceNativeMenuReload(); // used for testing + static char GetLocalizedAccelKey(const char *shortcutID); + static void ResetNativeApplicationMenu(); + +protected: + void ConstructNativeMenus(); + void ConstructFallbackNativeMenus(); + nsresult InsertMenuAtIndex(nsMenuX* aMenu, uint32_t aIndex); + void RemoveMenuAtIndex(uint32_t aIndex); + void HideItem(nsIDOMDocument* inDoc, const nsAString & inID, nsIContent** outHiddenNode); + void AquifyMenuBar(); + NSMenuItem* CreateNativeAppMenuItem(nsMenuX* inMenu, const nsAString& nodeID, SEL action, + int tag, NativeMenuItemTarget* target); + nsresult CreateApplicationMenu(nsMenuX* inMenu); + + nsTArray<mozilla::UniquePtr<nsMenuX>> mMenuArray; + nsIWidget* mParentWindow; // [weak] + GeckoNSMenu* mNativeMenu; // root menu, representing entire menu bar +}; + +#endif // nsMenuBarX_h_ diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm new file mode 100644 index 000000000..ff25eb81f --- /dev/null +++ b/widget/cocoa/nsMenuBarX.mm @@ -0,0 +1,979 @@ +/* -*- 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 "nsCocoaWindow.h" +#include "nsChildView.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsGkAtoms.h" +#include "nsObjCExceptions.h" +#include "nsThreadUtils.h" + +#include "nsIContent.h" +#include "nsIWidget.h" +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIAppStartup.h" +#include "nsIStringBundle.h" +#include "nsToolkitCompsCID.h" + +NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil; +nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr; +NSMenu* sApplicationMenu = nil; +BOOL sApplicationMenuIsFallback = NO; +BOOL gSomeMenuBarPainted = NO; + +// 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* sQuitItemContent = nullptr; + +NS_IMPL_ISUPPORTS(nsNativeMenuServiceX, nsINativeMenuService) + +NS_IMETHODIMP nsNativeMenuServiceX::CreateNativeMenuBar(nsIWidget* aParent, nsIContent* aMenuBarNode) +{ + NS_ASSERTION(NS_IsMainThread(), "Attempting to create native menu bar on wrong thread!"); + + RefPtr<nsMenuBarX> mb = new nsMenuBarX(); + if (!mb) + return NS_ERROR_OUT_OF_MEMORY; + + return mb->Create(aParent, aMenuBarNode); +} + +nsMenuBarX::nsMenuBarX() +: nsMenuGroupOwnerX(), mParentWindow(nullptr) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"]; + + 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; + + // make sure we unregister ourselves as a content observer + if (mContent) { + UnregisterForContentChanges(mContent); + } + + // We have to manually clear the array here because clearing causes menu items + // to call back into the menu bar to unregister themselves. We don't want to + // depend on member variable ordering to ensure that the array gets cleared + // before the registration hash table is destroyed. + mMenuArray.Clear(); + + [mNativeMenu release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsresult nsMenuBarX::Create(nsIWidget* aParent, nsIContent* aContent) +{ + if (!aParent) + return NS_ERROR_INVALID_ARG; + + mParentWindow = aParent; + mContent = aContent; + + if (mContent) { + AquifyMenuBar(); + + nsresult rv = nsMenuGroupOwnerX::Create(mContent); + if (NS_FAILED(rv)) + return rv; + + RegisterForContentChanges(mContent, this); + ConstructNativeMenus(); + } else { + ConstructFallbackNativeMenus(); + } + + // Give this to the parent window. The parent takes ownership. + static_cast<nsCocoaWindow*>(mParentWindow)->SetMenuBar(this); + + return NS_OK; +} + +void nsMenuBarX::ConstructNativeMenus() +{ + uint32_t count = mContent->GetChildCount(); + for (uint32_t i = 0; i < count; i++) { + nsIContent *menuContent = mContent->GetChildAt(i); + if (menuContent && + menuContent->IsXULElement(nsGkAtoms::menu)) { + nsMenuX* newMenu = new nsMenuX(); + if (newMenu) { + nsresult rv = newMenu->Create(this, this, menuContent); + if (NS_SUCCEEDED(rv)) + InsertMenuAtIndex(newMenu, GetMenuCount()); + else + delete newMenu; + } + } + } +} + +void nsMenuBarX::ConstructFallbackNativeMenus() +{ + 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; + } + + nsXPIDLString labelUTF16; + nsXPIDLString keyUTF16; + + const char16_t* labelProp = u"quitMenuitem.label"; + const char16_t* keyProp = u"quitMenuitem.key"; + + stringBundle->GetStringFromName(labelProp, getter_Copies(labelUTF16)); + stringBundle->GetStringFromName(keyProp, getter_Copies(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]; + NSMenuItem* quitMenuItem = [[[NSMenuItem alloc] initWithTitle:labelStr + action:@selector(menuItemHit:) + keyEquivalent:keyStr] autorelease]; + [quitMenuItem setTarget:nsMenuBarX::sNativeEventTarget]; + [quitMenuItem setTag:eCommand_ID_Quit]; + [sApplicationMenu addItem:quitMenuItem]; + sApplicationMenuIsFallback = YES; +} + +uint32_t nsMenuBarX::GetMenuCount() +{ + return mMenuArray.Length(); +} + +bool nsMenuBarX::MenuContainsAppMenu() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return ([mNativeMenu numberOfItems] > 0 && + [[mNativeMenu itemAtIndex:0] submenu] == sApplicationMenu); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +nsresult nsMenuBarX::InsertMenuAtIndex(nsMenuX* aMenu, uint32_t aIndex) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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) { + nsresult rv = NS_OK; // avoid warning about rv being unused + rv = CreateApplicationMenu(aMenu); + NS_ASSERTION(NS_SUCCEEDED(rv), "Can't create Application menu"); + + // 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] setSubmenu:sApplicationMenu]; + } + + // add menu to array that owns our menus + mMenuArray.InsertElementAt(aIndex, aMenu); + + // hook up submenus + nsIContent* menuContent = aMenu->Content(); + if (menuContent->GetChildCount() > 0 && + !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) { + int insertionIndex = nsMenuUtilsX::CalculateNativeInsertionPoint(this, aMenu); + if (MenuContainsAppMenu()) + insertionIndex++; + [mNativeMenu insertItem:aMenu->NativeMenuItem() atIndex:insertionIndex]; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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; + } + + // 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 = mMenuArray[aIndex]->NativeMenuItem(); + int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem]; + if (nativeMenuItemIndex != -1) + [mNativeMenu removeItemAtIndex:nativeMenuItemIndex]; + + mMenuArray.RemoveElementAt(aIndex); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsMenuBarX::ObserveAttributeChanged(nsIDocument* aDocument, + nsIContent* aContent, + nsIAtom* aAttribute) +{ +} + +void nsMenuBarX::ObserveContentRemoved(nsIDocument* aDocument, + nsIContent* aChild, + int32_t aIndexInContainer) +{ + RemoveMenuAtIndex(aIndexInContainer); +} + +void nsMenuBarX::ObserveContentInserted(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild) +{ + nsMenuX* newMenu = new nsMenuX(); + if (newMenu) { + nsresult rv = newMenu->Create(this, this, aChild); + if (NS_SUCCEEDED(rv)) + InsertMenuAtIndex(newMenu, aContainer->IndexOf(aChild)); + else + delete newMenu; + } +} + +void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& indexString) +{ + NSString* locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading()) + length:indexString.Length()]; + NSArray* indexes = [locationString componentsSeparatedByString:@"|"]; + unsigned int indexCount = [indexes count]; + if (indexCount == 0) + return; + + nsMenuX* currentMenu = NULL; + 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++) { + nsMenuX* menu = mMenuArray[i].get(); + if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) { + visible++; + if (visible == (targetIndex + 1)) { + currentMenu = 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++) { + nsMenuObjectX* targetMenu = currentMenu->GetItemAt(j); + if (!targetMenu) + return; + if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(targetMenu->Content())) { + visible++; + if (targetMenu->MenuObjectType() == eSubmenuObjectType && visible == (targetIndex + 1)) { + currentMenu = static_cast<nsMenuX*>(targetMenu); + // fake open/close to cause lazy update to happen + currentMenu->MenuOpened(); + currentMenu->MenuClosed(); + break; + } + } + } + } +} + +// 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 NULL; + } + 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() +{ + nsMenuX* xulHelpMenu = GetXULHelpMenu(); + if (xulHelpMenu) { + NSMenu* helpMenu = (NSMenu*)xulHelpMenu->NativeData(); + if (helpMenu) + [NSApp setHelpMenu:helpMenu]; + } +} + +nsresult nsMenuBarX::Paint() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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 setMainMenu:mNativeMenu]; + SetSystemHelpMenu(); + nsMenuBarX::sLastGeckoMenuBarPainted = this; + + gSomeMenuBarPainted = YES; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Returns the 'key' attribute of the 'shortcutID' object (if any) in the +// currently active menubar's DOM document. 'shortcutID' should be the id +// (i.e. the name) of a component that defines a commonly used (and +// localized) cmd+key shortcut, and belongs to a keyset containing similar +// objects. For example "key_selectAll". Returns a value that can be +// compared to the first character of [NSEvent charactersIgnoringModifiers] +// when [NSEvent modifierFlags] == NSCommandKeyMask. +char nsMenuBarX::GetLocalizedAccelKey(const char *shortcutID) +{ + if (!sLastGeckoMenuBarPainted) + return 0; + + nsCOMPtr<nsIDOMDocument> domDoc(do_QueryInterface(sLastGeckoMenuBarPainted->mContent->OwnerDoc())); + if (!domDoc) + return 0; + + NS_ConvertASCIItoUTF16 shortcutIDStr((const char *)shortcutID); + nsCOMPtr<nsIDOMElement> shortcutElement; + domDoc->GetElementById(shortcutIDStr, getter_AddRefs(shortcutElement)); + nsCOMPtr<nsIContent> shortcutContent = do_QueryInterface(shortcutElement); + if (!shortcutContent) + return 0; + + nsAutoString key; + shortcutContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, key); + NS_LossyConvertUTF16toASCII keyASC(key.get()); + const char *keyASCPtr = keyASC.get(); + if (!keyASCPtr) + return 0; + // If keyID's 'key' attribute isn't exactly one character long, it's not + // what we're looking for. + if (strlen(keyASCPtr) != sizeof(char)) + return 0; + // Make sure retval is lower case. + char retval = tolower(keyASCPtr[0]); + + return retval; +} + +/* static */ +void nsMenuBarX::ResetNativeApplicationMenu() +{ + [sApplicationMenu removeAllItems]; + [sApplicationMenu release]; + sApplicationMenu = nil; + sApplicationMenuIsFallback = NO; +} + +// Hide the item in the menu by setting the 'hidden' attribute. Returns it in |outHiddenNode| so +// the caller can hang onto it if they so choose. It is acceptable to pass nsull +// for |outHiddenNode| if the caller doesn't care about the hidden node. +void nsMenuBarX::HideItem(nsIDOMDocument* inDoc, const nsAString & inID, nsIContent** outHiddenNode) +{ + nsCOMPtr<nsIDOMElement> menuItem; + inDoc->GetElementById(inID, getter_AddRefs(menuItem)); + nsCOMPtr<nsIContent> menuContent(do_QueryInterface(menuItem)); + if (menuContent) { + menuContent->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, NS_LITERAL_STRING("true"), false); + if (outHiddenNode) { + *outHiddenNode = menuContent.get(); + NS_IF_ADDREF(*outHiddenNode); + } + } +} + +// Do what is necessary to conform to the Aqua guidelines for menus. +void nsMenuBarX::AquifyMenuBar() +{ + nsCOMPtr<nsIDOMDocument> domDoc(do_QueryInterface(mContent->GetComposedDoc())); + if (domDoc) { + // remove the "About..." item and its separator + HideItem(domDoc, NS_LITERAL_STRING("aboutSeparator"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("aboutName"), getter_AddRefs(mAboutItemContent)); + if (!sAboutItemContent) + sAboutItemContent = mAboutItemContent; + + // remove quit item and its separator + HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent)); + 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, NS_LITERAL_STRING("menu_PrefsSeparator"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("menu_preferences"), getter_AddRefs(mPrefItemContent)); + if (!sPrefItemContent) + sPrefItemContent = mPrefItemContent; + + // hide items that we use for the Application menu + HideItem(domDoc, NS_LITERAL_STRING("menu_mac_services"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_app"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_others"), nullptr); + HideItem(domDoc, NS_LITERAL_STRING("menu_mac_show_all"), nullptr); + } +} + +// for creating menu items destined for the Application menu +NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* inMenu, const nsAString& nodeID, SEL action, + int tag, NativeMenuItemTarget* target) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + nsCOMPtr<nsIDocument> doc = inMenu->Content()->GetUncomposedDoc(); + if (!doc) { + return nil; + } + + nsCOMPtr<nsIDOMDocument> domdoc(do_QueryInterface(doc)); + if (!domdoc) { + return nil; + } + + // Get information from the gecko menu item + nsAutoString label; + nsAutoString modifiers; + nsAutoString key; + nsCOMPtr<nsIDOMElement> menuItem; + domdoc->GetElementById(nodeID, getter_AddRefs(menuItem)); + if (menuItem) { + menuItem->GetAttribute(NS_LITERAL_STRING("label"), label); + menuItem->GetAttribute(NS_LITERAL_STRING("modifiers"), modifiers); + menuItem->GetAttribute(NS_LITERAL_STRING("key"), key); + } + else { + return nil; + } + + // 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()) { + nsCOMPtr<nsIDOMElement> keyElement; + domdoc->GetElementById(key, getter_AddRefs(keyElement)); + if (keyElement) { + nsCOMPtr<nsIContent> keyContent (do_QueryInterface(keyElement)); + // first grab the key equivalent character + nsAutoString keyChar(NS_LITERAL_STRING(" ")); + keyContent->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; + keyContent->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:action keyEquivalent:keyEquiv]; + + [newMenuItem setTag:tag]; + [newMenuItem setTarget:target]; + [newMenuItem setKeyEquivalentModifierMask:macKeyModifiers]; + + MenuItemInfo * info = [[MenuItemInfo alloc] initWithMenuGroupOwner:this]; + [newMenuItem setRepresentedObject:info]; + [info release]; + + return newMenuItem; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// build the Application menu shared by all menu bars +nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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 + ======================== + = 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 + ======================== + = 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) { + // This code reads attributes we are going to care about from the DOM elements + + NSMenuItem *itemBeingAdded = nil; + BOOL addAboutSeparator = FALSE; + + // Add the About menu item + itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("aboutName"), @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(inMenu, NS_LITERAL_STRING("menu_preferences"), @selector(menuItemHit:), + eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget); + if (itemBeingAdded) { + [sApplicationMenu addItem:itemBeingAdded]; + [itemBeingAdded release]; + itemBeingAdded = nil; + + // Add separator after Preferences menu + [sApplicationMenu addItem:[NSMenuItem separatorItem]]; + } + + // Add Services menu item + itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_services"), 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 setSubmenu:servicesMenu]; + [NSApp setServicesMenu: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(inMenu, NS_LITERAL_STRING("menu_mac_hide_app"), @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(inMenu, NS_LITERAL_STRING("menu_mac_hide_others"), @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(inMenu, NS_LITERAL_STRING("menu_mac_show_all"), @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]]; + + // Add quit menu item + itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_FileQuitItem"), @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 setTarget:nsMenuBarX::sNativeEventTarget]; + [defaultQuitItem setTag:eCommand_ID_Quit]; + [sApplicationMenu addItem:defaultQuitItem]; + } + } + + return (sApplicationMenu) ? NS_OK : NS_ERROR_FAILURE; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void nsMenuBarX::SetParent(nsIWidget* aParent) +{ + mParentWindow = aParent; +} + +// +// 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 *)theEvent +{ + // 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:theEvent]; + } + + NSResponder *firstResponder = [keyWindow firstResponder]; + + gMenuItemsExecuteCommands = NO; + [super performKeyEquivalent:theEvent]; + 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; +} + +@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)sender +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!gMenuItemsExecuteCommands) { + return; + } + + int tag = [sender tag]; + + nsMenuGroupOwnerX* menuGroupOwner = nullptr; + nsMenuBarX* menuBar = nullptr; + MenuItemInfo* info = [sender representedObject]; + + if (info) { + menuGroupOwner = [info menuGroupOwner]; + if (!menuGroupOwner) { + return; + } + if (menuGroupOwner->MenuObjectType() == eMenuBarObjectType) { + menuBar = static_cast<nsMenuBarX*>(menuGroupOwner); + } + } + + // 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); + return; + } + else if (tag == eCommand_ID_Prefs) { + nsIContent* mostSpecificContent = sPrefItemContent; + if (menuBar && menuBar->mPrefItemContent) + mostSpecificContent = menuBar->mPrefItemContent; + nsMenuUtilsX::DispatchCommandTo(mostSpecificContent); + return; + } + else if (tag == eCommand_ID_HideApp) { + [NSApp hide:sender]; + return; + } + else if (tag == eCommand_ID_HideOthers) { + [NSApp hideOtherApplications:sender]; + return; + } + else if (tag == eCommand_ID_ShowAll) { + [NSApp unhideAllApplications:sender]; + return; + } + else 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); + } + else { + nsCOMPtr<nsIAppStartup> appStartup = do_GetService(NS_APPSTARTUP_CONTRACTID); + if (appStartup) { + appStartup->Quit(nsIAppStartup::eAttemptQuit); + } + } + return; + } + + // given the commandID, look it up in our hashtable and dispatch to + // that menu item. + if (menuGroupOwner) { + nsMenuItemX* menuItem = menuGroupOwner->GetMenuItemForCommandID(static_cast<uint32_t>(tag)); + if (menuItem) + menuItem->DoCommand(); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@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; + else + return realTarget ? self : nil; +} + +- (SEL) action +{ + SEL realAction = [super action]; + if (gMenuItemsExecuteCommands) + return realAction; + else + return realAction ? @selector(_doNothing:) : NULL; +} + +- (void) _doNothing:(id)sender +{ +} + +@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 *)newItem +{ + [self _overrideClassOfMenuItem:newItem]; + [super addItem:newItem]; +} + +- (NSMenuItem *)addItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv +{ + NSMenuItem * newItem = [super addItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv]; + [self _overrideClassOfMenuItem:newItem]; + return newItem; +} + +- (void)insertItem:(NSMenuItem *)newItem atIndex:(NSInteger)index +{ + [self _overrideClassOfMenuItem:newItem]; + [super insertItem:newItem atIndex:index]; +} + +- (NSMenuItem *)insertItemWithTitle:(NSString *)aString action:(SEL)aSelector keyEquivalent:(NSString *)keyEquiv atIndex:(NSInteger)index +{ + NSMenuItem * newItem = [super insertItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv atIndex:index]; + [self _overrideClassOfMenuItem:newItem]; + return newItem; +} + +- (void) _overrideClassOfMenuItem:(NSMenuItem *)menuItem +{ + if ([menuItem class] == [NSMenuItem class]) + object_setClass(menuItem, [GeckoServicesNSMenuItem class]); +} + +@end diff --git a/widget/cocoa/nsMenuBaseX.h b/widget/cocoa/nsMenuBaseX.h new file mode 100644 index 000000000..5b9f89c56 --- /dev/null +++ b/widget/cocoa/nsMenuBaseX.h @@ -0,0 +1,79 @@ +/* -*- 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 nsMenuBaseX_h_ +#define nsMenuBaseX_h_ + +#import <Foundation/Foundation.h> + +#include "nsCOMPtr.h" +#include "nsIContent.h" + +enum nsMenuObjectTypeX { + eMenuBarObjectType, + eSubmenuObjectType, + eMenuItemObjectType, + eStandaloneNativeMenuObjectType, +}; + +// All menu objects subclass this. +// Menu bars are owned by their top-level nsIWidgets. +// All other objects are memory-managed based on the DOM. +// Content removal deletes them immediately and nothing else should. +// Do not attempt to hold strong references to them or delete them. +class nsMenuObjectX +{ +public: + virtual ~nsMenuObjectX() { } + virtual nsMenuObjectTypeX MenuObjectType()=0; + virtual void* NativeData()=0; + nsIContent* Content() { return mContent; } + + /** + * Called when an icon of a menu item somewhere in this menu has updated. + * Menu objects with parents need to propagate the notification to their + * parent. + */ + virtual void IconUpdated() {} + +protected: + nsCOMPtr<nsIContent> mContent; +}; + + +// +// Object stored as "representedObject" for all menu items +// + +class nsMenuGroupOwnerX; + +@interface MenuItemInfo : NSObject +{ + nsMenuGroupOwnerX * mMenuGroupOwner; +} + +- (id) initWithMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner; +- (nsMenuGroupOwnerX *) menuGroupOwner; +- (void) setMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner; + +@end + + +// Special command IDs that we know Mac OS X does not use for anything else. +// We use these in place of carbon's IDs for these commands in order to stop +// Carbon from messing with our event handlers. See bug 346883. + +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_Last = 8 +}; + +#endif // nsMenuBaseX_h_ diff --git a/widget/cocoa/nsMenuGroupOwnerX.h b/widget/cocoa/nsMenuGroupOwnerX.h new file mode 100644 index 000000000..657f420b5 --- /dev/null +++ b/widget/cocoa/nsMenuGroupOwnerX.h @@ -0,0 +1,61 @@ +/* -*- 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 "nsMenuBaseX.h" +#include "nsIMutationObserver.h" +#include "nsHashKeys.h" +#include "nsDataHashtable.h" +#include "nsString.h" + +class nsMenuItemX; +class nsChangeObserver; +class nsIWidget; +class nsIContent; + +class nsMenuGroupOwnerX : public nsMenuObjectX, public nsIMutationObserver +{ +public: + nsMenuGroupOwnerX(); + + nsresult Create(nsIContent * aContent); + + void RegisterForContentChanges(nsIContent* aContent, + nsChangeObserver* aMenuObject); + void UnregisterForContentChanges(nsIContent* aContent); + uint32_t RegisterForCommand(nsMenuItemX* aItem); + void UnregisterCommand(uint32_t aCommandID); + nsMenuItemX* GetMenuItemForCommandID(uint32_t inCommandID); + void AddMenuItemInfoToSet(MenuItemInfo* info); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER + +protected: + virtual ~nsMenuGroupOwnerX(); + + nsChangeObserver* LookupContentChangeObserver(nsIContent* aContent); + + uint32_t mCurrentCommandID; // unique command id (per menu-bar) to + // give to next item that asks + + // stores observers for content change notification + nsDataHashtable<nsPtrHashKey<nsIContent>, nsChangeObserver *> mContentToObserverTable; + + // stores mapping of command IDs to menu objects + nsDataHashtable<nsUint32HashKey, nsMenuItemX *> mCommandToMenuObjectTable; + + // Stores references to all the MenuItemInfo objects created with weak + // references to us. They may live longer than we do, so when we're + // destroyed we need to clear all their weak references. This avoids + // crashes in -[NativeMenuItemTarget menuItemHit:]. See bug 1131473. + NSMutableSet* mInfoSet; +}; + +#endif // nsMenuGroupOwner_h_ diff --git a/widget/cocoa/nsMenuGroupOwnerX.mm b/widget/cocoa/nsMenuGroupOwnerX.mm new file mode 100644 index 000000000..661a52bd8 --- /dev/null +++ b/widget/cocoa/nsMenuGroupOwnerX.mm @@ -0,0 +1,261 @@ +/* -*- 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 "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" + +#include "nsINode.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsMenuGroupOwnerX, nsIMutationObserver) + + +nsMenuGroupOwnerX::nsMenuGroupOwnerX() +: mCurrentCommandID(eCommand_ID_Last) +{ + mInfoSet = [[NSMutableSet setWithCapacity:10] retain]; +} + + +nsMenuGroupOwnerX::~nsMenuGroupOwnerX() +{ + MOZ_ASSERT(mContentToObserverTable.Count() == 0, "have outstanding mutation observers!\n"); + + // The MenuItemInfo objects in mInfoSet may live longer than we do. So when + // we get destroyed we need to invalidate all their mMenuGroupOwner pointers. + NSEnumerator* counter = [mInfoSet objectEnumerator]; + MenuItemInfo* info; + while ((info = (MenuItemInfo*) [counter nextObject])) { + [info setMenuGroupOwner:nil]; + } + [mInfoSet release]; +} + + +nsresult nsMenuGroupOwnerX::Create(nsIContent* aContent) +{ + if (!aContent) + return NS_ERROR_INVALID_ARG; + + mContent = aContent; + + return NS_OK; +} + + +// +// nsIMutationObserver +// + + +void nsMenuGroupOwnerX::CharacterDataWillChange(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + + +void nsMenuGroupOwnerX::CharacterDataChanged(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + + +void nsMenuGroupOwnerX::ContentAppended(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aFirstNewContent, + int32_t /* unused */) +{ + for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) { + ContentInserted(aDocument, aContainer, cur, 0); + } +} + + +void nsMenuGroupOwnerX::NodeWillBeDestroyed(const nsINode * aNode) +{ +} + + +void nsMenuGroupOwnerX::AttributeWillChange(nsIDocument* aDocument, + dom::Element* aContent, + int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aNewValue) +{ +} + +void nsMenuGroupOwnerX::NativeAnonymousChildListChange(nsIDocument* aDocument, + nsIContent* aContent, + bool aIsRemove) +{ +} + +void nsMenuGroupOwnerX::AttributeChanged(nsIDocument* aDocument, + dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) +{ + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + nsChangeObserver* obs = LookupContentChangeObserver(aElement); + if (obs) + obs->ObserveAttributeChanged(aDocument, aElement, aAttribute); +} + + +void nsMenuGroupOwnerX::ContentRemoved(nsIDocument * aDocument, + nsIContent * aContainer, + nsIContent * aChild, + int32_t aIndexInContainer, + nsIContent * aPreviousSibling) +{ + if (!aContainer) { + return; + } + + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + nsChangeObserver* obs = LookupContentChangeObserver(aContainer); + if (obs) + obs->ObserveContentRemoved(aDocument, aChild, aIndexInContainer); + else if (aContainer != 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 = aContainer->GetParent(); + if (parent) { + obs = LookupContentChangeObserver(parent); + if (obs) + obs->ObserveContentRemoved(aDocument, aChild, aIndexInContainer); + } + } +} + + +void nsMenuGroupOwnerX::ContentInserted(nsIDocument * aDocument, + nsIContent * aContainer, + nsIContent * aChild, + int32_t /* unused */) +{ + if (!aContainer) { + return; + } + + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + nsChangeObserver* obs = LookupContentChangeObserver(aContainer); + if (obs) + obs->ObserveContentInserted(aDocument, aContainer, aChild); + else if (aContainer != 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 = aContainer->GetParent(); + if (parent) { + obs = LookupContentChangeObserver(parent); + if (obs) + obs->ObserveContentInserted(aDocument, aContainer, aChild); + } + } +} + + +void nsMenuGroupOwnerX::ParentChainChanged(nsIContent *aContent) +{ +} + + +// 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.Put(aContent, aMenuObject); +} + + +void nsMenuGroupOwnerX::UnregisterForContentChanges(nsIContent *aContent) +{ + if (mContentToObserverTable.Contains(aContent)) { + aContent->RemoveMutationObserver(this); + } + mContentToObserverTable.Remove(aContent); +} + + +nsChangeObserver* nsMenuGroupOwnerX::LookupContentChangeObserver(nsIContent* aContent) +{ + nsChangeObserver * result; + if (mContentToObserverTable.Get(aContent, &result)) + return result; + else + 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* inMenuItem) +{ + // 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.Put(mCurrentCommandID, inMenuItem); + + return mCurrentCommandID; +} + + +// Removes the mapping between the given 4-character command ID +// and its associated menu item. +void nsMenuGroupOwnerX::UnregisterCommand(uint32_t inCommandID) +{ + mCommandToMenuObjectTable.Remove(inCommandID); +} + + +nsMenuItemX* nsMenuGroupOwnerX::GetMenuItemForCommandID(uint32_t inCommandID) +{ + nsMenuItemX * result; + if (mCommandToMenuObjectTable.Get(inCommandID, &result)) + return result; + else + return nullptr; +} + +void nsMenuGroupOwnerX::AddMenuItemInfoToSet(MenuItemInfo* info) +{ + [mInfoSet addObject:info]; +} diff --git a/widget/cocoa/nsMenuItemIconX.h b/widget/cocoa/nsMenuItemIconX.h new file mode 100644 index 000000000..7352a94e2 --- /dev/null +++ b/widget/cocoa/nsMenuItemIconX.h @@ -0,0 +1,66 @@ +/* -*- 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_ + +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "imgINotificationObserver.h" + +class nsIURI; +class nsIContent; +class imgRequestProxy; +class nsMenuObjectX; + +#import <Cocoa/Cocoa.h> + +class nsMenuItemIconX : public imgINotificationObserver +{ +public: + nsMenuItemIconX(nsMenuObjectX* aMenuItem, + nsIContent* aContent, + NSMenuItem* aNativeMenuItem); +private: + virtual ~nsMenuItemIconX(); + +public: + NS_DECL_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + + // 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(); + + // GetIconURI fails if the item should not have any icon. + nsresult GetIconURI(nsIURI** aIconURI); + + // LoadIcon will set a placeholder image and start a load request for the + // icon. The request may not complete until after LoadIcon returns. + nsresult LoadIcon(nsIURI* aIconURI); + + // Unless we take precautions, we may outlive the object that created us + // (mMenuObject, which owns our native menu item (mNativeMenuItem)). + // Destroy() should be called from mMenuObject's destructor to prevent + // this from happening. See bug 499600. + void Destroy(); + +protected: + nsresult OnFrameComplete(imgIRequest* aRequest); + + nsCOMPtr<nsIContent> mContent; + RefPtr<imgRequestProxy> mIconRequest; + nsMenuObjectX* mMenuObject; // [weak] + nsIntRect mImageRegionRect; + bool mLoadedIcon; + bool mSetIcon; + NSMenuItem* mNativeMenuItem; // [weak] +}; + +#endif // nsMenuItemIconX_h_ diff --git a/widget/cocoa/nsMenuItemIconX.mm b/widget/cocoa/nsMenuItemIconX.mm new file mode 100644 index 000000000..7589c279e --- /dev/null +++ b/widget/cocoa/nsMenuItemIconX.mm @@ -0,0 +1,466 @@ +/* -*- 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 "nsMenuItemIconX.h" +#include "nsObjCExceptions.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsIDOMElement.h" +#include "nsICSSDeclaration.h" +#include "nsIDOMCSSValue.h" +#include "nsIDOMCSSPrimitiveValue.h" +#include "nsIDOMRect.h" +#include "nsThreadUtils.h" +#include "nsToolkit.h" +#include "nsNetUtil.h" +#include "imgLoader.h" +#include "imgRequestProxy.h" +#include "nsMenuItemX.h" +#include "gfxPlatform.h" +#include "imgIContainer.h" +#include "nsCocoaUtils.h" +#include "nsContentUtils.h" +#include "nsIContentPolicy.h" + +using mozilla::dom::Element; +using mozilla::gfx::SourceSurface; + +static const uint32_t kIconWidth = 16; +static const uint32_t kIconHeight = 16; + +typedef NS_STDCALL_FUNCPROTO(nsresult, GetRectSideMethod, nsIDOMRect, + GetBottom, (nsIDOMCSSPrimitiveValue**)); + +NS_IMPL_ISUPPORTS(nsMenuItemIconX, imgINotificationObserver) + +nsMenuItemIconX::nsMenuItemIconX(nsMenuObjectX* aMenuItem, + nsIContent* aContent, + NSMenuItem* aNativeMenuItem) +: mContent(aContent) +, mMenuObject(aMenuItem) +, mLoadedIcon(false) +, mSetIcon(false) +, mNativeMenuItem(aNativeMenuItem) +{ + // printf("Creating icon for menu item %d, menu %d, native item is %d\n", aMenuItem, aMenu, aNativeMenuItem); +} + +nsMenuItemIconX::~nsMenuItemIconX() +{ + if (mIconRequest) + mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); +} + +// Called from mMenuObjectX's destructor, to prevent us from outliving it +// (as might otherwise happen if calls to our imgINotificationObserver methods +// are still outstanding). mMenuObjectX owns our nNativeMenuItem. +void nsMenuItemIconX::Destroy() +{ + if (mIconRequest) { + mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); + mIconRequest = nullptr; + } + mMenuObject = nullptr; + mNativeMenuItem = nil; +} + +nsresult +nsMenuItemIconX::SetupIcon() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // Still don't have one, then something is wrong, get out of here. + if (!mNativeMenuItem) { + NS_ERROR("No native menu item"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIURI> iconURI; + nsresult rv = GetIconURI(getter_AddRefs(iconURI)); + if (NS_FAILED(rv)) { + // There is no icon for this menu item. An icon might have been set + // earlier. Clear it. + [mNativeMenuItem setImage:nil]; + + return NS_OK; + } + + rv = LoadIcon(iconURI); + 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. + [mNativeMenuItem setImage:nil]; + } + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +static int32_t +GetDOMRectSide(nsIDOMRect* aRect, GetRectSideMethod aMethod) +{ + nsCOMPtr<nsIDOMCSSPrimitiveValue> dimensionValue; + (aRect->*aMethod)(getter_AddRefs(dimensionValue)); + if (!dimensionValue) + return -1; + + uint16_t primitiveType; + nsresult rv = dimensionValue->GetPrimitiveType(&primitiveType); + if (NS_FAILED(rv) || primitiveType != nsIDOMCSSPrimitiveValue::CSS_PX) + return -1; + + float dimension = 0; + rv = dimensionValue->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX, + &dimension); + if (NS_FAILED(rv)) + return -1; + + return NSToIntRound(dimension); +} + +nsresult +nsMenuItemIconX::GetIconURI(nsIURI** aIconURI) +{ + if (!mMenuObject) + return NS_ERROR_FAILURE; + + // Mac native menu items support having both a checkmark and an icon + // simultaneously, but this is unheard of in the cross-platform toolkit, + // seemingly because the win32 theme is unable to cope with both at once. + // The downside is that it's possible to get a menu item marked with a + // native checkmark and a checkmark for an icon. Head off that possibility + // by pretending that no icon exists if this is a checkable menu item. + if (mMenuObject->MenuObjectType() == eMenuItemObjectType) { + nsMenuItemX* menuItem = static_cast<nsMenuItemX*>(mMenuObject); + if (menuItem->GetMenuItemType() != eRegularMenuItemType) + return NS_ERROR_FAILURE; + } + + if (!mContent) + return NS_ERROR_FAILURE; + + // First, look at the content node's "image" attribute. + nsAutoString imageURIString; + bool hasImageAttr = mContent->GetAttr(kNameSpaceID_None, + nsGkAtoms::image, + imageURIString); + + nsresult rv; + nsCOMPtr<nsIDOMCSSValue> cssValue; + nsCOMPtr<nsICSSDeclaration> cssStyleDecl; + nsCOMPtr<nsIDOMCSSPrimitiveValue> primitiveValue; + uint16_t primitiveType; + if (!hasImageAttr) { + // If the content node has no "image" attribute, get the + // "list-style-image" property from CSS. + nsCOMPtr<nsIDocument> document = mContent->GetComposedDoc(); + if (!document) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsPIDOMWindowInner> window = document->GetInnerWindow(); + if (!window) + return NS_ERROR_FAILURE; + + nsCOMPtr<Element> domElement = do_QueryInterface(mContent); + if (!domElement) + return NS_ERROR_FAILURE; + + ErrorResult dummy; + cssStyleDecl = window->GetComputedStyle(*domElement, EmptyString(), dummy); + dummy.SuppressException(); + if (!cssStyleDecl) + return NS_ERROR_FAILURE; + + NS_NAMED_LITERAL_STRING(listStyleImage, "list-style-image"); + rv = cssStyleDecl->GetPropertyCSSValue(listStyleImage, + getter_AddRefs(cssValue)); + if (NS_FAILED(rv)) return rv; + + primitiveValue = do_QueryInterface(cssValue); + if (!primitiveValue) return NS_ERROR_FAILURE; + + rv = primitiveValue->GetPrimitiveType(&primitiveType); + if (NS_FAILED(rv)) return rv; + if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_URI) + return NS_ERROR_FAILURE; + + rv = primitiveValue->GetStringValue(imageURIString); + if (NS_FAILED(rv)) return rv; + } + + // Empty the mImageRegionRect initially as the image region CSS could + // have been changed and now have an error or have been removed since the + // last GetIconURI call. + mImageRegionRect.SetEmpty(); + + // If this menu item shouldn't have an icon, the string will be empty, + // and NS_NewURI will fail. + nsCOMPtr<nsIURI> iconURI; + rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString); + if (NS_FAILED(rv)) return rv; + + *aIconURI = iconURI; + NS_ADDREF(*aIconURI); + + if (!hasImageAttr) { + // Check if the icon has a specified image region so that it can be + // cropped appropriately before being displayed. + NS_NAMED_LITERAL_STRING(imageRegion, "-moz-image-region"); + rv = cssStyleDecl->GetPropertyCSSValue(imageRegion, + getter_AddRefs(cssValue)); + // Just return NS_OK if there if there is a failure due to no + // moz-image region specified so the whole icon will be drawn anyway. + if (NS_FAILED(rv)) return NS_OK; + + primitiveValue = do_QueryInterface(cssValue); + if (!primitiveValue) return NS_OK; + + rv = primitiveValue->GetPrimitiveType(&primitiveType); + if (NS_FAILED(rv)) return NS_OK; + if (primitiveType != nsIDOMCSSPrimitiveValue::CSS_RECT) + return NS_OK; + + nsCOMPtr<nsIDOMRect> imageRegionRect; + rv = primitiveValue->GetRectValue(getter_AddRefs(imageRegionRect)); + if (NS_FAILED(rv)) return NS_OK; + + if (imageRegionRect) { + // Return NS_ERROR_FAILURE if the image region is invalid so the image + // is not drawn, and behavior is similar to XUL menus. + int32_t bottom = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetBottom); + int32_t right = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetRight); + int32_t top = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetTop); + int32_t left = GetDOMRectSide(imageRegionRect, &nsIDOMRect::GetLeft); + + if (top < 0 || left < 0 || bottom <= top || right <= left) + return NS_ERROR_FAILURE; + + mImageRegionRect.SetRect(left, top, right - left, bottom - top); + } + } + + return NS_OK; +} + +nsresult +nsMenuItemIconX::LoadIcon(nsIURI* aIconURI) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (mIconRequest) { + // Another icon request is already in flight. Kill it. + mIconRequest->Cancel(NS_BINDING_ABORTED); + mIconRequest = nullptr; + } + + mLoadedIcon = false; + + if (!mContent) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIDocument> document = mContent->OwnerDoc(); + + nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup(); + if (!loadGroup) return NS_ERROR_FAILURE; + + RefPtr<imgLoader> loader = nsContentUtils::GetImgLoaderForDocument(document); + if (!loader) return NS_ERROR_FAILURE; + + if (!mSetIcon) { + // Set a completely transparent 16x16 image as the icon on this menu item + // as a placeholder. This keeps the menu item text displayed in the same + // position that it will be displayed when the real icon is loaded, and + // prevents it from jumping around or looking misaligned. + + static bool sInitializedPlaceholder; + static NSImage* sPlaceholderIconImage; + if (!sInitializedPlaceholder) { + sInitializedPlaceholder = true; + + // Note that we only create the one and reuse it forever, so this is not a leak. + sPlaceholderIconImage = [[NSImage alloc] initWithSize:NSMakeSize(kIconWidth, kIconHeight)]; + } + + if (!sPlaceholderIconImage) return NS_ERROR_FAILURE; + + if (mNativeMenuItem) + [mNativeMenuItem setImage:sPlaceholderIconImage]; + } + + nsresult rv = loader->LoadImage(aIconURI, nullptr, nullptr, + mozilla::net::RP_Default, + nullptr, loadGroup, this, + nullptr, nullptr, nsIRequest::LOAD_NORMAL, nullptr, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, EmptyString(), + getter_AddRefs(mIconRequest)); + if (NS_FAILED(rv)) return rv; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// +// imgINotificationObserver +// + +NS_IMETHODIMP +nsMenuItemIconX::Notify(imgIRequest* aRequest, + int32_t aType, + const nsIntRect* aData) +{ + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + // Make sure the image loaded successfully. + uint32_t status = imgIRequest::STATUS_ERROR; + if (NS_FAILED(aRequest->GetImageStatus(&status)) || + (status & imgIRequest::STATUS_ERROR)) { + mIconRequest->Cancel(NS_BINDING_ABORTED); + mIconRequest = nullptr; + return NS_ERROR_FAILURE; + } + + nsCOMPtr<imgIContainer> image; + aRequest->GetImage(getter_AddRefs(image)); + MOZ_ASSERT(image); + + // Ask the image to decode at its intrinsic size. + int32_t width = 0, height = 0; + image->GetWidth(&width); + image->GetHeight(&height); + image->RequestDecodeForSize(nsIntSize(width, height), imgIContainer::FLAG_NONE); + } + + if (aType == imgINotificationObserver::FRAME_COMPLETE) { + return OnFrameComplete(aRequest); + } + + if (aType == imgINotificationObserver::DECODE_COMPLETE) { + if (mIconRequest && mIconRequest == aRequest) { + mIconRequest->Cancel(NS_BINDING_ABORTED); + mIconRequest = nullptr; + } + } + + return NS_OK; +} + +nsresult +nsMenuItemIconX::OnFrameComplete(imgIRequest* aRequest) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (aRequest != mIconRequest) + return NS_ERROR_FAILURE; + + // Only support one frame. + if (mLoadedIcon) + return NS_OK; + + if (!mNativeMenuItem) + return NS_ERROR_FAILURE; + + nsCOMPtr<imgIContainer> imageContainer; + aRequest->GetImage(getter_AddRefs(imageContainer)); + if (!imageContainer) { + [mNativeMenuItem setImage:nil]; + return NS_ERROR_FAILURE; + } + + int32_t origWidth = 0, origHeight = 0; + imageContainer->GetWidth(&origWidth); + imageContainer->GetHeight(&origHeight); + + // If the image region is invalid, don't draw the image to almost match + // the behavior of other platforms. + if (!mImageRegionRect.IsEmpty() && + (mImageRegionRect.XMost() > origWidth || + mImageRegionRect.YMost() > origHeight)) { + [mNativeMenuItem setImage:nil]; + return NS_ERROR_FAILURE; + } + + if (mImageRegionRect.IsEmpty()) { + mImageRegionRect.SetRect(0, 0, origWidth, origHeight); + } + + RefPtr<SourceSurface> surface = + imageContainer->GetFrame(imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_SYNC_DECODE); + if (!surface) { + [mNativeMenuItem setImage:nil]; + return NS_ERROR_FAILURE; + } + + CGImageRef origImage = NULL; + nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &origImage); + if (NS_FAILED(rv) || !origImage) { + [mNativeMenuItem setImage:nil]; + return NS_ERROR_FAILURE; + } + + bool createSubImage = !(mImageRegionRect.x == 0 && mImageRegionRect.y == 0 && + mImageRegionRect.width == origWidth && mImageRegionRect.height == origHeight); + + CGImageRef finalImage = origImage; + if (createSubImage) { + // if mImageRegionRect is set using CSS, we need to slice a piece out of the overall + // image to use as the icon + finalImage = ::CGImageCreateWithImageInRect(origImage, + ::CGRectMake(mImageRegionRect.x, + mImageRegionRect.y, + mImageRegionRect.width, + mImageRegionRect.height)); + ::CGImageRelease(origImage); + if (!finalImage) { + [mNativeMenuItem setImage:nil]; + return NS_ERROR_FAILURE; + } + } + + NSImage *newImage = nil; + rv = nsCocoaUtils::CreateNSImageFromCGImage(finalImage, &newImage); + if (NS_FAILED(rv) || !newImage) { + [mNativeMenuItem setImage:nil]; + ::CGImageRelease(finalImage); + return NS_ERROR_FAILURE; + } + + [newImage setSize:NSMakeSize(kIconWidth, kIconHeight)]; + [mNativeMenuItem setImage:newImage]; + + [newImage release]; + ::CGImageRelease(finalImage); + + mLoadedIcon = true; + mSetIcon = true; + + if (mMenuObject) { + mMenuObject->IconUpdated(); + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsMenuItemX.h b/widget/cocoa/nsMenuItemX.h new file mode 100644 index 000000000..67ae32c99 --- /dev/null +++ b/widget/cocoa/nsMenuItemX.h @@ -0,0 +1,75 @@ +/* -*- 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 "nsMenuBaseX.h" +#include "nsMenuGroupOwnerX.h" +#include "nsChangeObserver.h" + +#import <Cocoa/Cocoa.h> + +class nsString; +class nsMenuItemIconX; +class nsMenuX; + +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 : public nsMenuObjectX, + public nsChangeObserver +{ +public: + nsMenuItemX(); + virtual ~nsMenuItemX(); + + NS_DECL_CHANGEOBSERVER + + // nsMenuObjectX + void* NativeData() override {return (void*)mNativeMenuItem;} + nsMenuObjectTypeX MenuObjectType() override {return eMenuItemObjectType;} + + // nsMenuItemX + nsresult Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType, + nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode); + nsresult SetChecked(bool aIsChecked); + EMenuItemType GetMenuItemType(); + void DoCommand(); + nsresult DispatchDOMEvent(const nsString &eventName, bool* preventDefaultCalled); + void SetupIcon(); + +protected: + void UncheckRadioSiblings(nsIContent* inCheckedElement); + void SetKeyEquiv(); + + EMenuItemType mType; + // nsMenuItemX objects should always have a valid native menu item. + NSMenuItem* mNativeMenuItem; // [strong] + nsMenuX* mMenuParent; // [weak] + nsMenuGroupOwnerX* mMenuGroupOwner; // [weak] + nsCOMPtr<nsIContent> mCommandContent; + // The icon object should never outlive its creating nsMenuItemX object. + RefPtr<nsMenuItemIconX> mIcon; + bool mIsChecked; +}; + +#endif // nsMenuItemX_h_ diff --git a/widget/cocoa/nsMenuItemX.mm b/widget/cocoa/nsMenuItemX.mm new file mode 100644 index 000000000..114b69f43 --- /dev/null +++ b/widget/cocoa/nsMenuItemX.mm @@ -0,0 +1,369 @@ +/* -*- 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 "nsIWidget.h" +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMEvent.h" + +nsMenuItemX::nsMenuItemX() +{ + mType = eRegularMenuItemType; + mNativeMenuItem = nil; + mMenuParent = nullptr; + mMenuGroupOwner = nullptr; + mIsChecked = false; + + MOZ_COUNT_CTOR(nsMenuItemX); +} + +nsMenuItemX::~nsMenuItemX() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Prevent the icon object from outliving us. + if (mIcon) + mIcon->Destroy(); + + // autorelease the native menu item so that anything else happening to this + // object happens before the native menu item actually dies + [mNativeMenuItem autorelease]; + + if (mContent) + mMenuGroupOwner->UnregisterForContentChanges(mContent); + if (mCommandContent) + mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); + + MOZ_COUNT_DTOR(nsMenuItemX); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType, + nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + mType = aItemType; + mMenuParent = aParent; + mContent = aNode; + + mMenuGroupOwner = aMenuGroupOwner; + NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!"); + + mMenuGroupOwner->RegisterForContentChanges(mContent, this); + + nsIDocument *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->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand); + + if (!ourCommand.IsEmpty()) { + nsIContent *commandElement = doc->GetElementById(ourCommand); + + if (commandElement) { + mCommandContent = commandElement; + // register to observe the command DOM element + mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this); + } + } + } + + // decide enabled state based on command content if it exists, otherwise do it based + // on our own content + bool isEnabled; + if (mCommandContent) + isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); + else + isEnabled = !mContent->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:@""]; + + [mNativeMenuItem setEnabled:(BOOL)isEnabled]; + + SetChecked(mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, + nsGkAtoms::_true, eCaseMatters)); + SetKeyEquiv(); + } + + mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); + if (!mIcon) + return NS_ERROR_OUT_OF_MEMORY; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsMenuItemX::SetChecked(bool aIsChecked) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + mIsChecked = aIsChecked; + + // update the content model. This will also handle unchecking our siblings + // if we are a radiomenu + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, + mIsChecked ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false"), true); + + // update native menu item + if (mIsChecked) + [mNativeMenuItem setState:NSOnState]; + else + [mNativeMenuItem setState:NSOffState]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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() +{ + // flip "checked" state if we're a checkbox menu, or an un-checked radio menu + if (mType == eCheckboxMenuItemType || + (mType == eRadioMenuItemType && !mIsChecked)) { + if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, + nsGkAtoms::_false, eCaseMatters)) + SetChecked(!mIsChecked); + /* the AttributeChanged code will update all the internal state */ + } + + nsMenuUtilsX::DispatchCommandTo(mContent); +} + +nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled) +{ + if (!mContent) + return NS_ERROR_FAILURE; + + // get owner document for content + nsCOMPtr<nsIDocument> parentDoc = mContent->OwnerDoc(); + + // get interface for creating DOM events from content owner document + nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(parentDoc); + if (!domDoc) { + NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument"); + return NS_ERROR_FAILURE; + } + + // create DOM event + nsCOMPtr<nsIDOMEvent> event; + nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create nsIDOMEvent"); + return rv; + } + event->InitEvent(eventName, true, true); + + // mark DOM event as trusted + event->SetTrusted(true); + + // send DOM event + rv = mContent->DispatchEvent(event, preventDefaultCalled); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to send DOM event via EventTarget"); + return rv; + } + + return NS_OK; +} + +// Walk the sibling list looking for nodes with the same name and +// uncheck them all. +void nsMenuItemX::UncheckRadioSiblings(nsIContent* inCheckedContent) +{ + nsAutoString myGroupName; + inCheckedContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName); + if (!myGroupName.Length()) // no groupname, nothing to do + return; + + nsCOMPtr<nsIContent> parent = inCheckedContent->GetParent(); + if (!parent) + return; + + // loop over siblings + uint32_t count = parent->GetChildCount(); + for (uint32_t i = 0; i < count; i++) { + nsIContent *sibling = parent->GetChildAt(i); + if (sibling) { + if (sibling != inCheckedContent) { // skip this node + // if the current sibling is in the same group, clear it + if (sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, + myGroupName, eCaseMatters)) + sibling->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("false"), true); + } + } + } +} + +void nsMenuItemX::SetKeyEquiv() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Set key shortcut and modifiers + nsAutoString keyValue; + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); + if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) { + nsIContent *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.Assign(NS_LITERAL_STRING(" ")); + } + } + + nsAutoString modifiersStr; + keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr); + uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr); + + unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers); + [mNativeMenuItem setKeyEquivalentModifierMask:macModifiers]; + + NSString *keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get() + length:keyChar.Length()] lowercaseString]; + if ([keyEquivalent isEqualToString:@" "]) + [mNativeMenuItem setKeyEquivalent:@""]; + else + [mNativeMenuItem setKeyEquivalent:keyEquivalent]; + + return; + } + } + + // if the key was removed, clear the key + [mNativeMenuItem setKeyEquivalent:@""]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// +// nsChangeObserver +// + +void +nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *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) { + if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, + nsGkAtoms::_true, eCaseMatters)) + UncheckRadioSiblings(mContent); + } + mMenuParent->SetRebuild(true); + } + else if (aAttribute == nsGkAtoms::hidden || + aAttribute == nsGkAtoms::collapsed || + aAttribute == nsGkAtoms::label) { + mMenuParent->SetRebuild(true); + } + else if (aAttribute == nsGkAtoms::key) { + SetKeyEquiv(); + } + else if (aAttribute == nsGkAtoms::image) { + SetupIcon(); + } + else if (aAttribute == nsGkAtoms::disabled) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) + [mNativeMenuItem setEnabled:NO]; + else + [mNativeMenuItem setEnabled:YES]; + } + } + else if (aContent == mCommandContent) { + // 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->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled); + mContent->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->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + else + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true); + } + // now we sync our native menu item with the command DOM node + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) + [mNativeMenuItem setEnabled:NO]; + else + [mNativeMenuItem setEnabled:YES]; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsMenuItemX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer) +{ + if (aChild == mCommandContent) { + mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); + mCommandContent = nullptr; + } + + mMenuParent->SetRebuild(true); +} + +void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer, + nsIContent *aChild) +{ + mMenuParent->SetRebuild(true); +} + +void nsMenuItemX::SetupIcon() +{ + if (mIcon) + mIcon->SetupIcon(); +} diff --git a/widget/cocoa/nsMenuUtilsX.h b/widget/cocoa/nsMenuUtilsX.h new file mode 100644 index 000000000..1571cdfb0 --- /dev/null +++ b/widget/cocoa/nsMenuUtilsX.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 nsMenuUtilsX_h_ +#define nsMenuUtilsX_h_ + +#include "nscore.h" +#include "nsMenuBaseX.h" + +#import <Cocoa/Cocoa.h> + +class nsIContent; +class nsString; +class nsMenuBarX; + +// Namespace containing utility functions used in our native menu implementation. +namespace nsMenuUtilsX +{ + void DispatchCommandTo(nsIContent* aTargetContent); + 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* inContent); + int CalculateNativeInsertionPoint(nsMenuObjectX* aParent, nsMenuObjectX* aChild); +} // namespace nsMenuUtilsX + +#endif // nsMenuUtilsX_h_ diff --git a/widget/cocoa/nsMenuUtilsX.mm b/widget/cocoa/nsMenuUtilsX.mm new file mode 100644 index 000000000..db6471712 --- /dev/null +++ b/widget/cocoa/nsMenuUtilsX.mm @@ -0,0 +1,223 @@ +/* -*- 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/dom/Event.h" +#include "nsMenuUtilsX.h" +#include "nsMenuBarX.h" +#include "nsMenuX.h" +#include "nsMenuItemX.h" +#include "nsStandaloneNativeMenu.h" +#include "nsObjCExceptions.h" +#include "nsCocoaUtils.h" +#include "nsCocoaWindow.h" +#include "nsGkAtoms.h" +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMXULCommandEvent.h" +#include "nsPIDOMWindow.h" +#include "nsQueryObject.h" + +using namespace mozilla; + +void nsMenuUtilsX::DispatchCommandTo(nsIContent* aTargetContent) +{ + NS_PRECONDITION(aTargetContent, "null ptr"); + + nsIDocument* doc = aTargetContent->OwnerDoc(); + if (doc) { + ErrorResult rv; + RefPtr<dom::Event> event = + doc->CreateEvent(NS_LITERAL_STRING("xulcommandevent"), rv); + nsCOMPtr<nsIDOMXULCommandEvent> command = do_QueryObject(event); + + // FIXME: Should probably figure out how to init this with the actual + // pressed keys, but this is a big old edge case anyway. -dwh + if (command && + NS_SUCCEEDED(command->InitCommandEvent(NS_LITERAL_STRING("command"), + true, true, + doc->GetInnerWindow(), 0, + false, false, false, + false, nullptr))) { + event->SetTrusted(true); + bool dummy; + aTargetContent->DispatchEvent(event, &dummy); + } + } +} + +NSString* nsMenuUtilsX::GetTruncatedCocoaLabel(const nsString& itemLabel) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // 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_NIL; +} + +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 != NULL) { + 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 |= NSShiftKeyMask; + if (geckoModifiers & knsMenuItemAltModifier) + macModifiers |= NSAlternateKeyMask; + if (geckoModifiers & knsMenuItemControlModifier) + macModifiers |= NSControlKeyMask; + if (geckoModifiers & knsMenuItemCommandModifier) + macModifiers |= NSCommandKeyMask; + + return macModifiers; +} + +nsMenuBarX* nsMenuUtilsX::GetHiddenWindowMenuBar() +{ + nsIWidget* hiddenWindowWidgetNoCOMPtr = nsCocoaUtils::GetHiddenWindowWidget(); + if (hiddenWindowWidgetNoCOMPtr) + return static_cast<nsCocoaWindow*>(hiddenWindowWidgetNoCOMPtr)->GetMenuBar(); + else + return nullptr; +} + +// It would be nice if we could localize these edit menu names. +NSMenuItem* nsMenuUtilsX::GetStandardEditMenuItem() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // 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 setSubmenu: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_NIL; +} + +bool nsMenuUtilsX::NodeIsHiddenOrCollapsed(nsIContent* inContent) +{ + return (inContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters) || + inContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed, + nsGkAtoms::_true, eCaseMatters)); +} + +// Determines how many items are visible among the siblings in a menu that are +// before the given child. This will not count the application menu. +int nsMenuUtilsX::CalculateNativeInsertionPoint(nsMenuObjectX* aParent, + nsMenuObjectX* aChild) +{ + int insertionPoint = 0; + nsMenuObjectTypeX parentType = aParent->MenuObjectType(); + if (parentType == eMenuBarObjectType) { + nsMenuBarX* menubarParent = static_cast<nsMenuBarX*>(aParent); + uint32_t numMenus = menubarParent->GetMenuCount(); + for (uint32_t i = 0; i < numMenus; i++) { + nsMenuX* currMenu = menubarParent->GetMenuAt(i); + if (currMenu == aChild) + return insertionPoint; // we found ourselves, break out + if (currMenu && [currMenu->NativeMenuItem() menu]) + insertionPoint++; + } + } + else if (parentType == eSubmenuObjectType || + parentType == eStandaloneNativeMenuObjectType) { + nsMenuX* menuParent; + if (parentType == eSubmenuObjectType) + menuParent = static_cast<nsMenuX*>(aParent); + else + menuParent = static_cast<nsStandaloneNativeMenu*>(aParent)->GetMenuXObject(); + + uint32_t numItems = menuParent->GetItemCount(); + for (uint32_t i = 0; i < numItems; i++) { + // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2) + nsMenuObjectX* currItem = menuParent->GetItemAt(i); + if (currItem == aChild) + return insertionPoint; // we found ourselves, break out + NSMenuItem* nativeItem = nil; + nsMenuObjectTypeX currItemType = currItem->MenuObjectType(); + if (currItemType == eSubmenuObjectType) + nativeItem = static_cast<nsMenuX*>(currItem)->NativeMenuItem(); + else + nativeItem = (NSMenuItem*)(currItem->NativeData()); + if ([nativeItem menu]) + insertionPoint++; + } + } + return insertionPoint; +} diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h new file mode 100644 index 000000000..7b5146a0b --- /dev/null +++ b/widget/cocoa/nsMenuX.h @@ -0,0 +1,101 @@ +/* -*- 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/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsMenuBaseX.h" +#include "nsMenuBarX.h" +#include "nsMenuGroupOwnerX.h" +#include "nsCOMPtr.h" +#include "nsChangeObserver.h" + +class nsMenuX; +class nsMenuItemIconX; +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 +} +- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu; +@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 nsMenuX : public nsMenuObjectX, + public nsChangeObserver +{ +public: + nsMenuX(); + virtual ~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 + + // nsMenuObjectX + void* NativeData() override {return (void*)mNativeMenu;} + nsMenuObjectTypeX MenuObjectType() override {return eSubmenuObjectType;} + void IconUpdated() override { mParent->IconUpdated(); } + + // nsMenuX + nsresult Create(nsMenuObjectX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode); + uint32_t GetItemCount(); + nsMenuObjectX* GetItemAt(uint32_t aPos); + nsresult GetVisibleItemCount(uint32_t &aCount); + nsMenuObjectX* GetVisibleItemAt(uint32_t aPos); + nsEventStatus MenuOpened(); + void MenuClosed(); + void SetRebuild(bool aMenuEvent); + NSMenuItem* NativeMenuItem(); + nsresult SetupIcon(); + + static bool IsXULHelpMenu(nsIContent* aMenuContent); + +protected: + void MenuConstruct(); + nsresult RemoveAll(); + nsresult SetEnabled(bool aIsEnabled); + nsresult GetEnabled(bool* aIsEnabled); + void GetMenuPopupContent(nsIContent** aResult); + bool OnOpen(); + bool OnClose(); + nsresult AddMenuItem(nsMenuItemX* aMenuItem); + nsMenuX* AddMenu(mozilla::UniquePtr<nsMenuX> aMenu); + void LoadMenuItem(nsIContent* inMenuItemContent); + void LoadSubMenu(nsIContent* inMenuContent); + GeckoNSMenu* CreateMenuWithGeckoString(nsString& menuTitle); + + nsTArray<mozilla::UniquePtr<nsMenuObjectX>> mMenuObjectsArray; + nsString mLabel; + uint32_t mVisibleItemsCount; // cache + nsMenuObjectX* mParent; // [weak] + nsMenuGroupOwnerX* mMenuGroupOwner; // [weak] + // The icon object should never outlive its creating nsMenuX object. + RefPtr<nsMenuItemIconX> mIcon; + GeckoNSMenu* mNativeMenu; // [strong] + MenuDelegate* mMenuDelegate; // [strong] + // nsMenuX objects should always have a valid native menu item. + NSMenuItem* mNativeMenuItem; // [strong] + bool mIsEnabled; + bool mDestroyHandlerCalled; + bool mNeedsRebuild; + bool mConstructed; + bool mVisible; + bool mXBLAttached; +}; + +#endif // nsMenuX_h_ diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm new file mode 100644 index 000000000..757221eac --- /dev/null +++ b/widget/cocoa/nsMenuX.mm @@ -0,0 +1,1051 @@ +/* -*- 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 <dlfcn.h> + +#include "nsMenuX.h" +#include "nsMenuItemX.h" +#include "nsMenuUtilsX.h" +#include "nsMenuItemIconX.h" +#include "nsStandaloneNativeMenu.h" + +#include "nsObjCExceptions.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 "nsIDocument.h" +#include "nsIContent.h" +#include "nsIDOMDocument.h" +#include "nsIDocumentObserver.h" +#include "nsIComponentManager.h" +#include "nsIRollupListener.h" +#include "nsIDOMElement.h" +#include "nsBindingManager.h" +#include "nsIServiceManager.h" +#include "nsXULPopupManager.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "jsapi.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsIXPConnect.h" + +#include "mozilla/MouseEvents.h" + +using namespace mozilla; + +static bool gConstructingMenu = false; +static bool gMenuMethodsSwizzled = false; + +int32_t nsMenuX::sIndexingMenuLevel = 0; + + +// +// Objective-C class used for representedObject +// + +@implementation MenuItemInfo + +- (id) initWithMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner +{ + if ((self = [super init]) != nil) { + [self setMenuGroupOwner:aMenuGroupOwner]; + } + return self; +} + +- (void) dealloc +{ + [self setMenuGroupOwner:nullptr]; + [super dealloc]; +} + +- (nsMenuGroupOwnerX *) menuGroupOwner +{ + return mMenuGroupOwner; +} + +- (void) setMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner +{ + // weak reference as the nsMenuGroupOwnerX owns all of its sub-objects + mMenuGroupOwner = aMenuGroupOwner; + if (aMenuGroupOwner) { + aMenuGroupOwner->AddMenuItemInfoToSet(self); + } +} + +@end + + +// +// nsMenuX +// + +nsMenuX::nsMenuX() +: mVisibleItemsCount(0), mParent(nullptr), mMenuGroupOwner(nullptr), + mNativeMenu(nil), mNativeMenuItem(nil), mIsEnabled(true), + mDestroyHandlerCalled(false), mNeedsRebuild(true), + mConstructed(false), mVisible(true), mXBLAttached(false) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!gMenuMethodsSwizzled) { + 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; + } + + mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this]; + + if (!nsMenuBarX::sNativeEventTarget) + nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init]; + + MOZ_COUNT_CTOR(nsMenuX); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsMenuX::~nsMenuX() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Prevent the icon object from outliving us. + if (mIcon) + mIcon->Destroy(); + + RemoveAll(); + + [mNativeMenu setDelegate: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]; + + // alert the change notifier we don't care no more + if (mContent) + mMenuGroupOwner->UnregisterForContentChanges(mContent); + + MOZ_COUNT_DTOR(nsMenuX); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsresult nsMenuX::Create(nsMenuObjectX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + mContent = aNode; + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); + mNativeMenu = CreateMenuWithGeckoString(mLabel); + + // register this menu to be notified when changes are made to our content object + mMenuGroupOwner = aMenuGroupOwner; // weak ref + NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one"); + mMenuGroupOwner->RegisterForContentChanges(mContent, this); + + mParent = aParent; + // our parent could be either a menu bar (if we're toplevel) or a menu (if we're a submenu) + +#ifdef DEBUG + nsMenuObjectTypeX parentType = +#endif + mParent->MenuObjectType(); + NS_ASSERTION((parentType == eMenuBarObjectType || parentType == eSubmenuObjectType || parentType == eStandaloneNativeMenuObjectType), + "Menu parent not a menu bar, menu, or native menu!"); + + if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent)) + mVisible = false; + if (mContent->GetChildCount() == 0) + mVisible = false; + + NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); + mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""]; + [mNativeMenuItem setSubmenu:mNativeMenu]; + + SetEnabled(!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters)); + + // We call MenuConstruct here because keyboard commands are dependent upon + // native menu items being created. If we only call MenuConstruct when a menu + // is actually selected, then we can't access keyboard commands until the + // menu gets selected, which is bad. + MenuConstruct(); + + mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsMenuX::AddMenuItem(nsMenuItemX* aMenuItem) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!aMenuItem) + return NS_ERROR_INVALID_ARG; + + mMenuObjectsArray.AppendElement(aMenuItem); + if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(aMenuItem->Content())) + return NS_OK; + ++mVisibleItemsCount; + + NSMenuItem* newNativeMenuItem = (NSMenuItem*)aMenuItem->NativeData(); + + // add the menu item to this menu + [mNativeMenu addItem:newNativeMenuItem]; + + // set up target/action + [newNativeMenuItem setTarget:nsMenuBarX::sNativeEventTarget]; + [newNativeMenuItem setAction:@selector(menuItemHit:)]; + + // set its command. we get the unique command id from the menubar + [newNativeMenuItem setTag:mMenuGroupOwner->RegisterForCommand(aMenuItem)]; + MenuItemInfo * info = [[MenuItemInfo alloc] initWithMenuGroupOwner:mMenuGroupOwner]; + [newNativeMenuItem setRepresentedObject:info]; + [info release]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsMenuX* nsMenuX::AddMenu(UniquePtr<nsMenuX> aMenu) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + // aMenu transfers ownership to mMenuObjectsArray and becomes nullptr, so + // we need to keep a raw pointer to access it conveniently. + nsMenuX* menu = aMenu.get(); + mMenuObjectsArray.AppendElement(Move(aMenu)); + + if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) { + return menu; + } + + ++mVisibleItemsCount; + + // We have to add a menu item and then associate the menu with it + NSMenuItem* newNativeMenuItem = menu->NativeMenuItem(); + if (newNativeMenuItem) { + [mNativeMenu addItem:newNativeMenuItem]; + [newNativeMenuItem setSubmenu:(NSMenu*)menu->NativeData()]; + } + + return menu; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(nullptr); +} + +// Includes all items, including hidden/collapsed ones +uint32_t nsMenuX::GetItemCount() +{ + return mMenuObjectsArray.Length(); +} + +// Includes all items, including hidden/collapsed ones +nsMenuObjectX* nsMenuX::GetItemAt(uint32_t aPos) +{ + if (aPos >= (uint32_t)mMenuObjectsArray.Length()) + return NULL; + + return mMenuObjectsArray[aPos].get(); +} + +// 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 +nsMenuObjectX* nsMenuX::GetVisibleItemAt(uint32_t aPos) +{ + + uint32_t count = mMenuObjectsArray.Length(); + if (aPos >= mVisibleItemsCount || aPos >= count) + return NULL; + + // If there are no invisible items, can provide direct access + if (mVisibleItemsCount == count) + return mMenuObjectsArray[aPos].get(); + + // Otherwise, traverse the array until we find the the item we're looking for. + nsMenuObjectX* item; + uint32_t visibleNodeIndex = 0; + for (uint32_t i = 0; i < count; i++) { + item = mMenuObjectsArray[i].get(); + if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(item->Content())) { + if (aPos == visibleNodeIndex) { + // we found the visible node we're looking for, return it + return item; + } + visibleNodeIndex++; + } + } + + return NULL; +} + +nsresult nsMenuX::RemoveAll() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (mNativeMenu) { + // clear command id's + int itemCount = [mNativeMenu numberOfItems]; + for (int i = 0; i < itemCount; i++) + mMenuGroupOwner->UnregisterCommand((uint32_t)[[mNativeMenu itemAtIndex:i] tag]); + // get rid of Cocoa menu items + for (int i = [mNativeMenu numberOfItems] - 1; i >= 0; i--) + [mNativeMenu removeItemAtIndex:i]; + } + + mMenuObjectsArray.Clear(); + mVisibleItemsCount = 0; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsEventStatus nsMenuX::MenuOpened() +{ + // Open the node. + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, NS_LITERAL_STRING("true"), true); + + // Fire a handler. If we're told to stop, don't build the menu at all + bool keepProcessing = OnOpen(); + + if (!mNeedsRebuild || !keepProcessing) + return nsEventStatus_eConsumeNoDefault; + + if (!mConstructed || mNeedsRebuild) { + if (mNeedsRebuild) + RemoveAll(); + + MenuConstruct(); + mConstructed = true; + } + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULPopupShown, nullptr, + WidgetMouseEvent::eReal); + + nsCOMPtr<nsIContent> popupContent; + GetMenuPopupContent(getter_AddRefs(popupContent)); + nsIContent* dispatchTo = popupContent ? popupContent : mContent; + dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); + + return nsEventStatus_eConsumeNoDefault; +} + +void nsMenuX::MenuClosed() +{ + if (mConstructed) { + // Don't close if a handler tells us to stop. + if (!OnClose()) + return; + + if (mNeedsRebuild) + mConstructed = false; + + mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true); + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULPopupHidden, nullptr, + WidgetMouseEvent::eReal); + + nsCOMPtr<nsIContent> popupContent; + GetMenuPopupContent(getter_AddRefs(popupContent)); + nsIContent* dispatchTo = popupContent ? popupContent : mContent; + dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); + + mDestroyHandlerCalled = true; + mConstructed = false; + } +} + +void nsMenuX::MenuConstruct() +{ + mConstructed = false; + gConstructingMenu = true; + + // reset destroy handler flag so that we'll know to fire it next time this menu goes away. + mDestroyHandlerCalled = false; + + //printf("nsMenuX::MenuConstruct called for %s = %d \n", NS_LossyConvertUTF16toASCII(mLabel).get(), mNativeMenu); + + // Retrieve our menupopup. + nsCOMPtr<nsIContent> menuPopup; + GetMenuPopupContent(getter_AddRefs(menuPopup)); + if (!menuPopup) { + gConstructingMenu = false; + return; + } + + // bug 365405: Manually wrap the menupopup node to make sure it's bounded + if (!mXBLAttached) { + nsresult rv; + nsCOMPtr<nsIXPConnect> xpconnect = + do_GetService(nsIXPConnect::GetCID(), &rv); + if (NS_SUCCEEDED(rv)) { + nsIDocument* ownerDoc = menuPopup->OwnerDoc(); + dom::AutoJSAPI jsapi; + if (ownerDoc && jsapi.Init(ownerDoc->GetInnerWindow())) { + JSContext* cx = jsapi.cx(); + JS::RootedObject ignoredObj(cx); + xpconnect->WrapNative(cx, JS::CurrentGlobalOrNull(cx), menuPopup, + NS_GET_IID(nsISupports), ignoredObj.address()); + mXBLAttached = true; + } + } + } + + // Iterate over the kids + uint32_t count = menuPopup->GetChildCount(); + for (uint32_t i = 0; i < count; i++) { + nsIContent *child = menuPopup->GetChildAt(i); + if (child) { + // depending on the type, create a menu item, separator, or submenu + if (child->IsAnyOfXULElements(nsGkAtoms::menuitem, + nsGkAtoms::menuseparator)) { + LoadMenuItem(child); + } else if (child->IsXULElement(nsGkAtoms::menu)) { + LoadSubMenu(child); + } + } + } // for each menu item + + gConstructingMenu = false; + mNeedsRebuild = false; + // printf("Done building, mMenuObjectsArray.Count() = %d \n", mMenuObjectsArray.Count()); +} + +void nsMenuX::SetRebuild(bool aNeedsRebuild) +{ + if (!gConstructingMenu) + mNeedsRebuild = aNeedsRebuild; +} + +nsresult nsMenuX::SetEnabled(bool aIsEnabled) +{ + if (aIsEnabled != mIsEnabled) { + // we always want to rebuild when this changes + mIsEnabled = aIsEnabled; + [mNativeMenuItem setEnabled:(BOOL)mIsEnabled]; + } + return NS_OK; +} + +nsresult nsMenuX::GetEnabled(bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + *aIsEnabled = mIsEnabled; + return NS_OK; +} + +GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& menuTitle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSString* title = [NSString stringWithCharacters:(UniChar*)menuTitle.get() length:menuTitle.Length()]; + GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title]; + [myMenu setDelegate: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 setAutoenablesItems: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_NIL; +} + +void nsMenuX::LoadMenuItem(nsIContent* inMenuItemContent) +{ + if (!inMenuItemContent) + return; + + nsAutoString menuitemName; + inMenuItemContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, menuitemName); + + // printf("menuitem %s \n", NS_LossyConvertUTF16toASCII(menuitemName).get()); + + EMenuItemType itemType = eRegularMenuItemType; + if (inMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) { + itemType = eSeparatorMenuItemType; + } + else { + static nsIContent::AttrValuesArray strings[] = + {&nsGkAtoms::checkbox, &nsGkAtoms::radio, nullptr}; + switch (inMenuItemContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + strings, eCaseMatters)) { + case 0: itemType = eCheckboxMenuItemType; break; + case 1: itemType = eRadioMenuItemType; break; + } + } + + // Create the item. + nsMenuItemX* menuItem = new nsMenuItemX(); + if (!menuItem) + return; + + nsresult rv = menuItem->Create(this, menuitemName, itemType, mMenuGroupOwner, inMenuItemContent); + if (NS_FAILED(rv)) { + delete menuItem; + return; + } + + AddMenuItem(menuItem); + + // This needs to happen after the nsIMenuItem object is inserted into + // our item array in AddMenuItem() + menuItem->SetupIcon(); +} + +void nsMenuX::LoadSubMenu(nsIContent* inMenuContent) +{ + auto menu = MakeUnique<nsMenuX>(); + if (!menu) + return; + + nsresult rv = menu->Create(this, mMenuGroupOwner, inMenuContent); + if (NS_FAILED(rv)) + return; + + // |menu|'s ownership is transfer to AddMenu but, if it is successfully + // added, we can access it via the returned raw pointer. + nsMenuX* menu_ptr = AddMenu(Move(menu)); + + // This needs to happen after the nsIMenu object is inserted into + // our item array in AddMenu() + if (menu_ptr) { + menu_ptr->SetupIcon(); + } +} + +// This menu is about to open. Returns TRUE if we should keep processing the event, +// FALSE if the handler wants to stop the opening of the menu. +bool nsMenuX::OnOpen() +{ + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULPopupShowing, nullptr, + WidgetMouseEvent::eReal); + + nsCOMPtr<nsIContent> popupContent; + GetMenuPopupContent(getter_AddRefs(popupContent)); + + nsresult rv = NS_OK; + nsIContent* dispatchTo = popupContent ? popupContent : mContent; + rv = dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); + if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) + return false; + + // 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. + + // Get new popup content first since it might have changed as a result of the + // eXULPopupShowing event above. + GetMenuPopupContent(getter_AddRefs(popupContent)); + if (!popupContent) + return true; + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->UpdateMenuItems(popupContent); + } + + return true; +} + +// Returns TRUE if we should keep processing the event, FALSE if the handler +// wants to stop the closing of the menu. +bool nsMenuX::OnClose() +{ + if (mDestroyHandlerCalled) + return true; + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULPopupHiding, nullptr, + WidgetMouseEvent::eReal); + + nsCOMPtr<nsIContent> popupContent; + GetMenuPopupContent(getter_AddRefs(popupContent)); + + nsresult rv = NS_OK; + nsIContent* dispatchTo = popupContent ? popupContent : mContent; + rv = dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); + + mDestroyHandlerCalled = true; + + if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) + return false; + + return true; +} + +// 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). +void nsMenuX::GetMenuPopupContent(nsIContent** aResult) +{ + if (!aResult) + return; + *aResult = nullptr; + + // Check to see if we are a "menupopup" node (if we are a native menu). + { + int32_t dummy; + nsCOMPtr<nsIAtom> tag = mContent->OwnerDoc()->BindingManager()->ResolveTag(mContent, &dummy); + if (tag == nsGkAtoms::menupopup) { + *aResult = mContent; + NS_ADDREF(*aResult); + return; + } + } + + // Otherwise check our child nodes. + + uint32_t count = mContent->GetChildCount(); + + for (uint32_t i = 0; i < count; i++) { + int32_t dummy; + nsIContent *child = mContent->GetChildAt(i); + nsCOMPtr<nsIAtom> tag = child->OwnerDoc()->BindingManager()->ResolveTag(child, &dummy); + if (tag == nsGkAtoms::menupopup) { + *aResult = child; + NS_ADDREF(*aResult); + return; + } + } +} + +NSMenuItem* nsMenuX::NativeMenuItem() +{ + return mNativeMenuItem; +} + +bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) +{ + bool retval = false; + if (aMenuContent) { + nsAutoString id; + aMenuContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); + if (id.Equals(NS_LITERAL_STRING("helpMenu"))) + retval = true; + } + return retval; +} + +// +// nsChangeObserver +// + +void nsMenuX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, + nsIAtom *aAttribute) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // ignore the |open| attribute, which is by far the most common + if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) + return; + + nsMenuObjectTypeX parentType = mParent->MenuObjectType(); + + if (aAttribute == nsGkAtoms::disabled) { + SetEnabled(!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters)); + } + else if (aAttribute == nsGkAtoms::label) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); + + // invalidate my parent. If we're a submenu parent, we have to rebuild + // the parent menu in order for the changes to be picked up. If we're + // a regular menu, just change the title and redraw the menubar. + if (parentType == eMenuBarObjectType) { + // reuse the existing menu, to avoid rebuilding the root menu bar. + NS_ASSERTION(mNativeMenu, "nsMenuX::AttributeChanged: invalid menu handle."); + NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); + [mNativeMenu setTitle:newCocoaLabelString]; + } + else if (parentType == eSubmenuObjectType) { + static_cast<nsMenuX*>(mParent)->SetRebuild(true); + } + else if (parentType == eStandaloneNativeMenuObjectType) { + static_cast<nsStandaloneNativeMenu*>(mParent)->GetMenuXObject()->SetRebuild(true); + } + } + else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) { + SetRebuild(true); + + bool contentIsHiddenOrCollapsed = nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); + + // don't do anything if the state is correct already + if (contentIsHiddenOrCollapsed != mVisible) + return; + + if (contentIsHiddenOrCollapsed) { + if (parentType == eMenuBarObjectType || + parentType == eSubmenuObjectType || + parentType == eStandaloneNativeMenuObjectType) { + NSMenu* parentMenu = (NSMenu*)mParent->NativeData(); + // An exception will get thrown if we try to remove an item that isn't + // in the menu. + if ([parentMenu indexOfItem:mNativeMenuItem] != -1) + [parentMenu removeItem:mNativeMenuItem]; + mVisible = false; + } + } + else { + if (parentType == eMenuBarObjectType || + parentType == eSubmenuObjectType || + parentType == eStandaloneNativeMenuObjectType) { + int insertionIndex = nsMenuUtilsX::CalculateNativeInsertionPoint(mParent, this); + if (parentType == eMenuBarObjectType) { + // Before inserting we need to figure out if we should take the native + // application menu into account. + nsMenuBarX* mb = static_cast<nsMenuBarX*>(mParent); + if (mb->MenuContainsAppMenu()) + insertionIndex++; + } + NSMenu* parentMenu = (NSMenu*)mParent->NativeData(); + [parentMenu insertItem:mNativeMenuItem atIndex:insertionIndex]; + [mNativeMenuItem setSubmenu:mNativeMenu]; + mVisible = true; + } + } + } + else if (aAttribute == nsGkAtoms::image) { + SetupIcon(); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsMenuX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, + int32_t aIndexInContainer) +{ + if (gConstructingMenu) + return; + + SetRebuild(true); + mMenuGroupOwner->UnregisterForContentChanges(aChild); +} + +void nsMenuX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer, + nsIContent *aChild) +{ + if (gConstructingMenu) + return; + + SetRebuild(true); +} + +nsresult nsMenuX::SetupIcon() +{ + // In addition to out-of-memory, menus that are children of the menu bar + // will not have mIcon set. + if (!mIcon) + return NS_ERROR_OUT_OF_MEMORY; + + return mIcon->SetupIcon(); +} + +// +// MenuDelegate Objective-C class, used to set up Carbon events +// + +@implementation MenuDelegate + +- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ((self = [super init])) { + NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL gecko menu! Will crash!"); + mGeckoMenu = geckoMenu; + } + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item +{ + if (!menu || !item || !mGeckoMenu) + return; + + nsMenuObjectX* target = mGeckoMenu->GetVisibleItemAt((uint32_t)[menu indexOfItem:item]); + if (target && (target->MenuObjectType() == eMenuItemObjectType)) { + nsMenuItemX* targetMenuItem = static_cast<nsMenuItemX*>(target); + bool handlerCalledPreventDefault; // but we don't actually care + targetMenuItem->DispatchDOMEvent(NS_LITERAL_STRING("DOMMenuItemActive"), &handlerCalledPreventDefault); + } +} + +- (void)menuWillOpen:(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; + + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + if (rollupListener) { + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (rollupWidget) { + rollupListener->Rollup(0, true, nullptr, nullptr); + [menu cancelTracking]; + return; + } + } + mGeckoMenu->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; + + mGeckoMenu->MenuClosed(); +} + +@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 +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + 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; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mTables) + [mTables release]; + if (mItem) + [mItem release]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (BOOL)hasTable:(NSMapTable *)aTable +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +// Does nothing if aTable (its index value) is already present in mTables. +- (int)addTable:(NSMapTable *)aTable +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (aTable) + [mTables addObject:[NSValue valueWithPointer:aTable]]; + return [mTables count]; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +- (int)removeTable:(NSMapTable *)aTable +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (aTable) { + NSValue *objectToRemove = + [mTables member:[NSValue valueWithPointer:aTable]]; + if (objectToRemove) + [mTables removeObject:objectToRemove]; + } + return [mTables count]; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +@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 +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + 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]; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; + + [self nsMenuX_NSMenu_addItem:aItem toTable:aTable]; +} + ++ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem *)aItem fromTable:(NSMapTable *)aTable +{ + [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable]; + + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + 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]; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@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 000000000..23f2bc4d3 --- /dev/null +++ b/widget/cocoa/nsNativeThemeCocoa.h @@ -0,0 +1,178 @@ +/* -*- 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 "nsITheme.h" +#include "nsCOMPtr.h" +#include "nsIAtom.h" +#include "nsNativeTheme.h" + +@class CellDrawView; +@class NSProgressBarCell; +@class ContextAwareSearchFieldCell; +class nsDeviceContext; +struct SegmentedControlRenderSettings; + +namespace mozilla { +class EventStates; +} // namespace mozilla + +class nsNativeThemeCocoa : private nsNativeTheme, + public nsITheme +{ +public: + enum { + eThemeGeometryTypeTitlebar = eThemeGeometryTypeUnknown + 1, + eThemeGeometryTypeToolbar, + eThemeGeometryTypeToolbox, + eThemeGeometryTypeWindowButtons, + eThemeGeometryTypeFullscreenButton, + eThemeGeometryTypeMenu, + eThemeGeometryTypeHighlightedMenuItem, + eThemeGeometryTypeVibrancyLight, + eThemeGeometryTypeVibrancyDark, + eThemeGeometryTypeTooltip, + eThemeGeometryTypeSheet, + eThemeGeometryTypeSourceList, + eThemeGeometryTypeSourceListSelection, + eThemeGeometryTypeActiveSourceListSelection + }; + + nsNativeThemeCocoa(); + + NS_DECL_ISUPPORTS_INHERITED + + // The nsITheme interface. + NS_IMETHOD DrawWidgetBackground(nsRenderingContext* aContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + const nsRect& aRect, + const nsRect& aDirtyRect) override; + NS_IMETHOD GetWidgetBorder(nsDeviceContext* aContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + nsIntMargin* aResult) override; + + virtual bool GetWidgetPadding(nsDeviceContext* aContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + nsIntMargin* aResult) override; + + virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + uint8_t aWidgetType, nsRect* aOverflowRect) override; + + NS_IMETHOD GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame, + uint8_t aWidgetType, + mozilla::LayoutDeviceIntSize* aResult, bool* aIsOverridable) override; + NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, uint8_t aWidgetType, + nsIAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) override; + NS_IMETHOD ThemeChanged() override; + bool ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, uint8_t aWidgetType) override; + bool WidgetIsContainer(uint8_t aWidgetType) override; + bool ThemeDrawsFocusForWidget(uint8_t aWidgetType) override; + bool ThemeNeedsComboboxDropmarker() override; + virtual bool WidgetAppearanceDependsOnWindowFocus(uint8_t aWidgetType) override; + virtual bool NeedToClearBackgroundBehindWidget(nsIFrame* aFrame, + uint8_t aWidgetType) override; + virtual bool WidgetProvidesFontSmoothingBackgroundColor(nsIFrame* aFrame, uint8_t aWidgetType, + nscolor* aColor) override; + virtual ThemeGeometryType ThemeGeometryTypeForWidget(nsIFrame* aFrame, + uint8_t aWidgetType) override; + virtual Transparency GetWidgetTransparency(nsIFrame* aFrame, uint8_t aWidgetType) override; + + void DrawProgress(CGContextRef context, const HIRect& inBoxRect, + bool inIsIndeterminate, bool inIsHorizontal, + double inValue, double inMaxValue, nsIFrame* aFrame); + + static void DrawNativeTitlebar(CGContextRef aContext, CGRect aTitlebarRect, + CGFloat aUnifiedHeight, BOOL aIsMain, BOOL aIsFlipped); + +protected: + virtual ~nsNativeThemeCocoa(); + + nsIntMargin DirectionAwareMargin(const nsIntMargin& aMargin, nsIFrame* aFrame); + nsIFrame* SeparatorResponsibility(nsIFrame* aBefore, nsIFrame* aAfter); + CGRect SeparatorAdjustedRect(CGRect aRect, nsIFrame* aLeft, + nsIFrame* aCurrent, nsIFrame* aRight); + bool IsWindowSheet(nsIFrame* aFrame); + + // HITheme drawing routines + void DrawFrame(CGContextRef context, HIThemeFrameKind inKind, + const HIRect& inBoxRect, bool inReadOnly, + mozilla::EventStates inState); + void DrawMeter(CGContextRef context, const HIRect& inBoxRect, + nsIFrame* aFrame); + void DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect, + mozilla::EventStates inState, nsIFrame* aFrame, + const SegmentedControlRenderSettings& aSettings); + void DrawTabPanel(CGContextRef context, const HIRect& inBoxRect, nsIFrame* aFrame); + void DrawScale(CGContextRef context, const HIRect& inBoxRect, + mozilla::EventStates inState, bool inDirection, + bool inIsReverse, int32_t inCurrentValue, int32_t inMinValue, + int32_t inMaxValue, nsIFrame* aFrame); + void DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox, + const HIRect& inBoxRect, bool inSelected, + mozilla::EventStates inState, nsIFrame* aFrame); + void DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame* aFrame, mozilla::EventStates inState); + void DrawPushButton(CGContextRef cgContext, const HIRect& inBoxRect, + mozilla::EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame, float aOriginalHeight); + void DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect, + mozilla::EventStates inState, nsIFrame* aFrame, + const NSSize& aIconSize, NSString* aImageName, + bool aCenterHorizontally); + void DrawButton(CGContextRef context, ThemeButtonKind inKind, + const HIRect& inBoxRect, bool inIsDefault, + ThemeButtonValue inValue, ThemeButtonAdornment inAdornment, + mozilla::EventStates inState, nsIFrame* aFrame); + void DrawFocusOutline(CGContextRef cgContext, const HIRect& inBoxRect, + mozilla::EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame); + void DrawDropdown(CGContextRef context, const HIRect& inBoxRect, + mozilla::EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame); + void DrawSpinButtons(CGContextRef context, ThemeButtonKind inKind, + const HIRect& inBoxRect, ThemeDrawState inDrawState, + ThemeButtonAdornment inAdornment, + mozilla::EventStates inState, nsIFrame* aFrame); + void DrawSpinButton(CGContextRef context, ThemeButtonKind inKind, + const HIRect& inBoxRect, ThemeDrawState inDrawState, + ThemeButtonAdornment inAdornment, + mozilla::EventStates inState, + nsIFrame* aFrame, uint8_t aWidgetType); + void DrawUnifiedToolbar(CGContextRef cgContext, const HIRect& inBoxRect, + NSWindow* aWindow); + void DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame *aFrame); + void DrawResizer(CGContextRef cgContext, const HIRect& aRect, nsIFrame *aFrame); + + // Scrollbars + void GetScrollbarPressStates(nsIFrame *aFrame, + mozilla::EventStates aButtonStates[]); + nsIFrame* GetParentScrollbarFrame(nsIFrame *aFrame); + bool IsParentScrollbarRolledOver(nsIFrame* aFrame); + +private: + NSButtonCell* mDisclosureButtonCell; + NSButtonCell* mHelpButtonCell; + NSButtonCell* mPushButtonCell; + NSButtonCell* mRadioButtonCell; + NSButtonCell* mCheckboxCell; + ContextAwareSearchFieldCell* mSearchFieldCell; + NSPopUpButtonCell* mDropdownCell; + NSComboBoxCell* mComboBoxCell; + NSProgressBarCell* mProgressBarCell; + NSLevelIndicatorCell* mMeterBarCell; + CellDrawView* mCellDrawView; +}; + +#endif // nsNativeThemeCocoa_h_ diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm new file mode 100644 index 000000000..3c8695442 --- /dev/null +++ b/widget/cocoa/nsNativeThemeCocoa.mm @@ -0,0 +1,3931 @@ +/* -*- 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 "mozilla/gfx/2D.h" +#include "mozilla/gfx/Helpers.h" +#include "nsChildView.h" +#include "nsDeviceContext.h" +#include "nsLayoutUtils.h" +#include "nsObjCExceptions.h" +#include "nsNumberControlFrame.h" +#include "nsRangeFrame.h" +#include "nsRenderingContext.h" +#include "nsRect.h" +#include "nsSize.h" +#include "nsThemeConstants.h" +#include "nsIPresShell.h" +#include "nsPresContext.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIFrame.h" +#include "nsIAtom.h" +#include "nsNameSpaceManager.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsCocoaFeatures.h" +#include "nsCocoaWindow.h" +#include "nsNativeThemeColors.h" +#include "nsIScrollableFrame.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMeterElement.h" +#include "nsLookAndFeel.h" +#include "VibrancyManager.h" + +#include "gfxContext.h" +#include "gfxQuartzSurface.h" +#include "gfxQuartzNativeDrawing.h" +#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 + +// The purpose of this class is to provide objects that can be used when drawing +// NSCells using drawWithFrame:inView: without causing any harm. The only +// messages that will be sent to such an object are "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! +@interface CellDrawView : NSView + +@end; + +@implementation CellDrawView + +- (BOOL)isFlipped +{ + return YES; +} + +- (NSText*)currentEditor +{ + return nil; +} + +@end + +// These two classes don't actually add any behavior over NSButtonCell. Their +// purpose is to make it easy to distinguish NSCell objects that are used for +// drawing radio buttons / checkboxes from other cell types. +// The class names are made up, there are no classes with these names in AppKit. +// The reason we need them is that calling [cell setButtonType:NSRadioButton] +// doesn't leave an easy-to-check "marker" on the cell object - there is no +// -[NSButtonCell buttonType] method. +@interface RadioButtonCell : NSButtonCell +@end; +@implementation RadioButtonCell @end; +@interface CheckboxCell : NSButtonCell +@end; +@implementation CheckboxCell @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 bool +FocusIsDrawnByDrawWithFrame(NSCell* aCell) +{ +#if defined(MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_8 + // When building with the 10.8 SDK or higher, focus rings don't draw as part + // of -[NSCell drawWithFrame:inView:] and must be drawn by a separate call + // to -[NSCell drawFocusRingMaskWithFrame:inView:]; . + // See the NSButtonCell section under + // https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#X10_8Notes + return false; +#else + if (!nsCocoaFeatures::OnYosemiteOrLater()) { + // When building with the 10.7 SDK or lower, focus rings always draw as + // part of -[NSCell drawWithFrame:inView:] if the build is run on 10.9 or + // lower. + return true; + } + + // On 10.10, whether the focus ring is drawn as part of + // -[NSCell drawWithFrame:inView:] depends on the cell type. + // Radio buttons and checkboxes draw their own focus rings, other cell + // types need -[NSCell drawFocusRingMaskWithFrame:inView:]. + return [aCell isKindOfClass:[RadioButtonCell class]] || + [aCell isKindOfClass:[CheckboxCell class]]; +#endif +} + +static void +DrawCellIncludingFocusRing(NSCell* aCell, NSRect aWithFrame, NSView* aInView) +{ + [aCell drawWithFrame:aWithFrame inView:aInView]; + + if (!FocusIsDrawnByDrawWithFrame(aCell)) { + 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 == NSRegularControlSize) { + tdi.kind = mIsIndeterminate ? kThemeLargeIndeterminateBar + : kThemeLargeProgressBar; + } else { + NS_ASSERTION(size == NSSmallControlSize, + "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 ContextAwareSearchFieldCell : NSSearchFieldCell +{ + nsIFrame* mContext; +} + +// setContext: stores the searchfield nsIFrame so that it can be consulted +// during painting. Please reset this by calling setContext:nullptr as soon as +// you're done with painting because we don't want to keep a dangling pointer. +- (void)setContext:(nsIFrame*)aContext; +@end + +@implementation ContextAwareSearchFieldCell + +- (id)initTextCell:(NSString*)aString +{ + if ((self = [super initTextCell:aString])) { + mContext = nullptr; + } + return self; +} + +- (void)setContext:(nsIFrame*)aContext +{ + mContext = aContext; +} + +static BOOL IsToolbarStyleContainer(nsIFrame* aFrame) +{ + nsIContent* content = aFrame->GetContent(); + if (!content) + return NO; + + if (content->IsAnyOfXULElements(nsGkAtoms::toolbar, + nsGkAtoms::toolbox, + nsGkAtoms::statusbar)) + return YES; + + switch (aFrame->StyleDisplay()->mAppearance) { + case NS_THEME_TOOLBAR: + case NS_THEME_STATUSBAR: + return YES; + default: + return NO; + } +} + +- (BOOL)_isToolbarMode +{ + // On 10.7, searchfields have two different styles, depending on whether + // the searchfield is on top of of window chrome. This function is called on + // 10.7 during drawing in order to determine which style to use. + for (nsIFrame* frame = mContext; frame; frame = frame->GetParent()) { + if (IsToolbarStyleContainer(frame)) { + return YES; + } + } + return NO; +} + +@end + +// Workaround for Bug 542048 +// On 64-bit, NSSearchFieldCells don't draw focus rings. +#if defined(__x86_64__) + +@interface SearchFieldCellWithFocusRing : ContextAwareSearchFieldCell {} @end + +@implementation SearchFieldCellWithFocusRing + +- (void)drawWithFrame:(NSRect)rect inView:(NSView*)controlView +{ + [super drawWithFrame:rect inView:controlView]; + + if (FocusIsDrawnByDrawWithFrame(self)) { + // For some reason, -[NSSearchFieldCell drawWithFrame:inView] doesn't draw a + // focus ring in 64 bit mode, no matter what SDK is used or what OS X version + // we're running on. But if FocusIsDrawnByDrawWithFrame(self), then our + // caller expects us to draw a focus ring. So we just do that here. + DrawFocusRingForCellIfNeeded(self, rect, controlView); + } +} + +- (void)drawFocusRingMaskWithFrame:(NSRect)rect inView:(NSView*)controlView +{ + // By default this draws nothing. I don't know why. + // We just draw the search field again. It's a great mask shape for its own + // focus ring. + [super drawWithFrame:rect inView:controlView]; +} + +@end + +#endif + +#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 == NSMiniControlSize) + return miniControlSize; + else if (cocoaControlSize == NSSmallControlSize) + return smallControlSize; + else + return regularControlSize; +} + +static NSControlSize CocoaSizeForEnum(int32_t enumControlSize) { + if (enumControlSize == miniControlSize) + return NSMiniControlSize; + else if (enumControlSize == smallControlSize) + return NSSmallControlSize; + else + return NSRegularControlSize; +} + +static NSString* CUIControlSizeForCocoaSize(NSControlSize aControlSize) +{ + if (aControlSize == NSRegularControlSize) + return @"regular"; + else if (aControlSize == NSSmallControlSize) + return @"small"; + else + return @"mini"; +} + +static void InflateControlRect(NSRect* rect, NSControlSize cocoaControlSize, const float marginSet[][3][4]) +{ + if (!marginSet) + return; + + static int osIndex = nsCocoaFeatures::OnYosemiteOrLater() ? + yosemiteOSorlater : leopardOSorlater; + 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 ChildView* ChildViewForFrame(nsIFrame* aFrame) +{ + if (!aFrame) + return nil; + + nsIWidget* widget = aFrame->GetNearestWidget(); + if (!widget) + return nil; + + NSView* view = (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET); + return [view isKindOfClass:[ChildView class]] ? (ChildView*)view : nil; +} + +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 = frame->GetParent()) { + if (frame->StyleDisplay()->mAppearance == NS_THEME_MAC_SOURCE_LIST) { + return true; + } + } + return false; +} + +NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeCocoa, nsNativeTheme, nsITheme) + +nsNativeThemeCocoa::nsNativeThemeCocoa() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + kMaxFocusRingWidth = nsCocoaFeatures::OnYosemiteOrLater() ? 7 : 4; + + // 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 = [[RadioButtonCell alloc] initTextCell:@""]; + [mRadioButtonCell setButtonType:NSRadioButton]; + + mCheckboxCell = [[CheckboxCell alloc] initTextCell:@""]; + [mCheckboxCell setButtonType:NSSwitchButton]; + [mCheckboxCell setAllowsMixedState:YES]; + +#if defined(__x86_64__) + mSearchFieldCell = [[SearchFieldCellWithFocusRing alloc] initTextCell:@""]; +#else + mSearchFieldCell = [[ContextAwareSearchFieldCell alloc] initTextCell:@""]; +#endif + [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]; + + mCellDrawView = [[CellDrawView alloc] init]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsNativeThemeCocoa::~nsNativeThemeCocoa() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mMeterBarCell release]; + [mProgressBarCell release]; + [mDisclosureButtonCell release]; + [mHelpButtonCell release]; + [mPushButtonCell release]; + [mRadioButtonCell release]; + [mCheckboxCell release]; + [mSearchFieldCell release]; + [mDropdownCell release]; + [mComboBoxCell release]; + [mCellDrawView release]; + + NS_OBJC_END_TRY_ABORT_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_ABORT_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_ABORT_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 NSRegularControlSize 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_ABORT_BLOCK; + + const float rectWidth = destRect.size.width, rectHeight = destRect.size.height; + const NSSize *sizes = settings.naturalSizes; + const NSSize miniSize = sizes[EnumSizeForCocoaSize(NSMiniControlSize)]; + const NSSize smallSize = sizes[EnumSizeForCocoaSize(NSSmallControlSize)]; + const NSSize regularSize = sizes[EnumSizeForCocoaSize(NSRegularControlSize)]; + + 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 = NSRegularControlSize; + 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_ABORT_BLOCK; +} + +@interface NSWindow(CoreUIRendererPrivate) ++ (CUIRendererRef)coreUIRenderer; +@end + +static id +GetAquaAppearance() +{ + // We only need NSAppearance on 10.10 and up. + if (nsCocoaFeatures::OnYosemiteOrLater()) { + Class NSAppearanceClass = NSClassFromString(@"NSAppearance"); + if (NSAppearanceClass && + [NSAppearanceClass respondsToSelector:@selector(appearanceNamed:)]) { + return [NSAppearanceClass performSelector:@selector(appearanceNamed:) + withObject:@"NSAppearanceNameAqua"]; + } + } + return nil; +} + +@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) +{ + id appearance = GetAquaAppearance(); + + if (!aSkipAreaCheck && aRect.size.width * aRect.size.height > BITMAP_MAX_AREA) { + return; + } + + 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 nsStyleCoord& va = aFrame->StyleDisplay()->mVerticalAlign; + uint8_t intval = (va.GetUnit() == eStyleUnit_Enumerated) + ? va.GetIntValue() + : NS_STYLE_VERTICAL_ALIGN_MIDDLE; + switch (intval) { + case NS_STYLE_VERTICAL_ALIGN_TOP: + case NS_STYLE_VERTICAL_ALIGN_TEXT_TOP: + return 0.0f; + + case NS_STYLE_VERTICAL_ALIGN_SUB: + case NS_STYLE_VERTICAL_ALIGN_SUPER: + case NS_STYLE_VERTICAL_ALIGN_MIDDLE: + case NS_STYLE_VERTICAL_ALIGN_MIDDLE_WITH_BASELINE: + return 0.5f; + + case NS_STYLE_VERTICAL_ALIGN_BASELINE: + case NS_STYLE_VERTICAL_ALIGN_TEXT_BOTTOM: + case NS_STYLE_VERTICAL_ALIGN_BOTTOM: + return 1.0f; + + default: + NS_NOTREACHED("invalid vertical-align"); + return 0.5f; + } +} + +// 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 + } + } +}; + +void +nsNativeThemeCocoa::DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox, + const HIRect& inBoxRect, bool inSelected, + EventStates inState, nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NSButtonCell *cell = inCheckbox ? mCheckboxCell : mRadioButtonCell; + NSCellStateValue state = inSelected ? NSOnState : NSOffState; + + // Check if we have an indeterminate checkbox + if (inCheckbox && GetIndeterminate(aFrame)) + state = NSMixedState; + + [cell setEnabled:!IsDisabled(aFrame, inState)]; + [cell setShowsFirstResponder:inState.HasState(NS_EVENT_STATE_FOCUS)]; + [cell setState:state]; + [cell setHighlighted:inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)]; + [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [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); + + DrawCellWithSnapping(cell, cgContext, drawRect, + inCheckbox ? checkboxSettings : radioSettings, + VerticalAlignFactor(aFrame), mCellDrawView, NO); + + NS_OBJC_END_TRY_ABORT_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 + } + } +}; + +void +nsNativeThemeCocoa::DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame* aFrame, EventStates inState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + ContextAwareSearchFieldCell* cell = mSearchFieldCell; + [cell setContext:aFrame]; + [cell setEnabled:!IsDisabled(aFrame, inState)]; + // NOTE: this could probably use inState + [cell setShowsFirstResponder:IsFocused(aFrame)]; + + // When using the 10.11 SDK, the default string will be shown if we don't + // set the placeholder string. + [cell setPlaceholderString:@""]; + + DrawCellWithSnapping(cell, cgContext, inBoxRect, searchFieldSettings, + VerticalAlignFactor(aFrame), mCellDrawView, + IsFrameRTL(aFrame)); + + [cell setContext:nullptr]; + + NS_OBJC_END_TRY_ABORT_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; + +void +nsNativeThemeCocoa::DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect, + EventStates inState, nsIFrame* aFrame, + const NSSize& aIconSize, NSString* aImageName, + bool aCenterHorizontally) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Adjust size and position of our drawRect. + CGFloat paddingX = std::max(CGFloat(0.0), aRect.size.width - aIconSize.width); + CGFloat paddingY = std::max(CGFloat(0.0), aRect.size.height - aIconSize.height); + CGFloat paddingStartX = std::min(paddingX, kMenuIconIndent); + CGFloat paddingEndX = std::max(CGFloat(0.0), paddingX - kMenuIconIndent); + CGRect drawRect = CGRectMake( + aRect.origin.x + (aCenterHorizontally ? ceil(paddingX / 2) : + IsFrameRTL(aFrame) ? paddingEndX : paddingStartX), + aRect.origin.y + ceil(paddingY / 2), + aIconSize.width, aIconSize.height); + + NSString* state = IsDisabled(aFrame, inState) ? @"disabled" : + (CheckBooleanAttr(aFrame, nsGkAtoms::menuactive) ? @"pressed" : @"normal"); + + NSString* imageName = aImageName; + if (!nsCocoaFeatures::OnElCapitanOrLater()) { + // Pre-10.11, image names are prefixed with "image." + imageName = [@"image." stringByAppendingString:aImageName]; + } + + 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_ABORT_BLOCK; +} + +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, + EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame, float aOriginalHeight) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + BOOL isActive = FrameIsInActiveWindow(aFrame); + BOOL isDisabled = IsDisabled(aFrame, inState); + + NSButtonCell* cell = (aWidgetType == NS_THEME_BUTTON) ? mPushButtonCell : + (aWidgetType == NS_THEME_MAC_HELP_BUTTON) ? mHelpButtonCell : mDisclosureButtonCell; + [cell setEnabled:!isDisabled]; + [cell setHighlighted:isActive && + inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)]; + [cell setShowsFirstResponder:inState.HasState(NS_EVENT_STATE_FOCUS) && !isDisabled && isActive]; + + if (aWidgetType != NS_THEME_BUTTON) { // Help button or disclosure button. + NSSize buttonSize = NSMakeSize(0, 0); + if (aWidgetType == NS_THEME_MAC_HELP_BUTTON) { + buttonSize = kHelpButtonSize; + } else { // Disclosure button. + buttonSize = kDisclosureButtonSize; + [cell setState:(aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED) ? NSOffState : NSOnState]; + } + + DrawCellWithScaling(cell, cgContext, inBoxRect, NSRegularControlSize, + NSZeroSize, buttonSize, NULL, mCellDrawView, + false); // Don't mirror icon in RTL. + } else { + // 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. + if (aOriginalHeight > DO_SQUARE_BUTTON_HEIGHT) { + [cell setBezelStyle:NSShadowlessSquareBezelStyle]; + DrawCellWithScaling(cell, cgContext, inBoxRect, NSRegularControlSize, + NSZeroSize, NSMakeSize(14, 0), NULL, mCellDrawView, + IsFrameRTL(aFrame)); + } else { + [cell setBezelStyle:NSRoundedBezelStyle]; + DrawCellWithSnapping(cell, cgContext, inBoxRect, pushButtonSettings, 0.5f, + mCellDrawView, IsFrameRTL(aFrame), 1.0f); + } + } + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawFocusOutline(CGContextRef cgContext, const HIRect& inBoxRect, + EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeFrameDrawInfo fdi; + fdi.version = 0; + fdi.kind = kHIThemeFrameTextFieldSquare; + fdi.state = kThemeStateActive; + fdi.isFocused = TRUE; + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + HIThemeDrawFrame(&inBoxRect, &fdi, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_ABORT_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); +} + +void +nsNativeThemeCocoa::DrawButton(CGContextRef cgContext, ThemeButtonKind inKind, + const HIRect& inBoxRect, bool inIsDefault, + ThemeButtonValue inValue, ThemeButtonAdornment inAdornment, + EventStates inState, nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + BOOL isActive = FrameIsInActiveWindow(aFrame); + BOOL isDisabled = IsDisabled(aFrame, inState); + + HIThemeButtonDrawInfo bdi; + bdi.version = 0; + bdi.kind = inKind; + bdi.value = inValue; + bdi.adornment = inAdornment; + + if (isDisabled) { + bdi.state = kThemeStateUnavailable; + } + else if (inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)) { + bdi.state = kThemeStatePressed; + } + else { + if (inKind == kThemeArrowButton) + bdi.state = kThemeStateUnavailable; // these are always drawn as unavailable + else if (!isActive && inKind == kThemeListHeaderButton) + bdi.state = kThemeStateInactive; + else + bdi.state = kThemeStateActive; + } + + if (inState.HasState(NS_EVENT_STATE_FOCUS) && isActive) + bdi.adornment |= kThemeAdornmentFocus; + + if (inIsDefault && !isDisabled && + !inState.HasState(NS_EVENT_STATE_ACTIVE)) { + bdi.adornment |= kThemeAdornmentDefault; + bdi.animation.time.start = 0; + bdi.animation.time.current = CFAbsoluteTimeGetCurrent(); + } + + HIRect drawFrame = inBoxRect; + + if (inKind == kThemePushButton) { + drawFrame.size.height -= 2; + if (inBoxRect.size.height < pushButtonSettings.naturalSizes[smallControlSize].height) { + bdi.kind = kThemePushButtonMini; + } + else if (inBoxRect.size.height < pushButtonSettings.naturalSizes[regularControlSize].height) { + bdi.kind = kThemePushButtonSmall; + drawFrame.origin.y -= 1; + drawFrame.origin.x += 1; + drawFrame.size.width -= 2; + } + } + else if (inKind == kThemeListHeaderButton) { + CGContextClipToRect(cgContext, inBoxRect); + // Always remove the top border. + drawFrame.origin.y -= 1; + drawFrame.size.height += 1; + // Remove the left border in LTR mode and the right border in RTL mode. + drawFrame.size.width += 1; + bool isLast = IsLastTreeHeaderCell(aFrame); + if (isLast) + drawFrame.size.width += 1; // Also remove the other border. + if (!IsFrameRTL(aFrame) || isLast) + drawFrame.origin.x -= 1; + } + + RenderTransformedHIThemeControl(cgContext, drawFrame, RenderButton, &bdi, + IsFrameRTL(aFrame)); + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + NS_OBJC_END_TRY_ABORT_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, + EventStates inState, uint8_t aWidgetType, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mDropdownCell setPullsDown:(aWidgetType == NS_THEME_BUTTON)]; + + BOOL isEditable = (aWidgetType == NS_THEME_MENULIST_TEXTFIELD); + NSCell* cell = isEditable ? (NSCell*)mComboBoxCell : (NSCell*)mDropdownCell; + + [cell setEnabled:!IsDisabled(aFrame, inState)]; + [cell setShowsFirstResponder:(IsFocused(aFrame) || inState.HasState(NS_EVENT_STATE_FOCUS))]; + [cell setHighlighted:IsOpenButton(aFrame)]; + [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [NSColor currentControlTint] : NSClearControlTint)]; + + const CellRenderSettings& settings = isEditable ? editableMenulistSettings : dropdownSettings; + DrawCellWithSnapping(cell, cgContext, inBoxRect, settings, + 0.5f, mCellDrawView, IsFrameRTL(aFrame)); + + NS_OBJC_END_TRY_ABORT_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 + } + } +}; + +void +nsNativeThemeCocoa::DrawSpinButtons(CGContextRef cgContext, ThemeButtonKind inKind, + const HIRect& inBoxRect, ThemeDrawState inDrawState, + ThemeButtonAdornment inAdornment, + EventStates inState, nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeButtonDrawInfo bdi; + bdi.version = 0; + bdi.kind = inKind; + bdi.value = kThemeButtonOff; + bdi.adornment = inAdornment; + + if (IsDisabled(aFrame, inState)) + bdi.state = kThemeStateUnavailable; + else + bdi.state = FrameIsInActiveWindow(aFrame) ? inDrawState : kThemeStateActive; + + HIThemeDrawButton(&inBoxRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawSpinButton(CGContextRef cgContext, + ThemeButtonKind inKind, + const HIRect& inBoxRect, + ThemeDrawState inDrawState, + ThemeButtonAdornment inAdornment, + EventStates inState, + nsIFrame* aFrame, + uint8_t aWidgetType) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_ASSERT(aWidgetType == NS_THEME_SPINNER_UPBUTTON || + aWidgetType == NS_THEME_SPINNER_DOWNBUTTON); + + HIThemeButtonDrawInfo bdi; + bdi.version = 0; + bdi.kind = inKind; + bdi.value = kThemeButtonOff; + bdi.adornment = inAdornment; + + if (IsDisabled(aFrame, inState)) + bdi.state = kThemeStateUnavailable; + else + bdi.state = FrameIsInActiveWindow(aFrame) ? inDrawState : kThemeStateActive; + + // 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 (aWidgetType == NS_THEME_SPINNER_DOWNBUTTON) { + 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_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawFrame(CGContextRef cgContext, HIThemeFrameKind inKind, + const HIRect& inBoxRect, bool inDisabled, + EventStates inState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeFrameDrawInfo fdi; + fdi.version = 0; + fdi.kind = inKind; + + // We don't ever set an inactive state for this because it doesn't + // look right (see other apps). + fdi.state = inDisabled ? kThemeStateUnavailable : kThemeStateActive; + + // for some reason focus rings on listboxes draw incorrectly + if (inKind == kHIThemeFrameListBox) + fdi.isFocused = 0; + else + fdi.isFocused = inState.HasState(NS_EVENT_STATE_FOCUS); + + // HIThemeDrawFrame takes the rect for the content area of the frame, not + // the bounding rect for the frame. Here we reduce the size of the rect we + // will pass to make it the size of the content. + HIRect drawRect = inBoxRect; + if (inKind == kHIThemeFrameTextFieldSquare) { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset); + drawRect.origin.x += frameOutset; + drawRect.origin.y += frameOutset; + drawRect.size.width -= frameOutset * 2; + drawRect.size.height -= frameOutset * 2; + } + else if (inKind == kHIThemeFrameListBox) { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset); + drawRect.origin.x += frameOutset; + drawRect.origin.y += frameOutset; + drawRect.size.width -= frameOutset * 2; + drawRect.size.height -= frameOutset * 2; + } + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + HIThemeDrawFrame(&drawRect, &fdi, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_ABORT_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 + } + } + } + } +}; + +void +nsNativeThemeCocoa::DrawProgress(CGContextRef cgContext, const HIRect& inBoxRect, + bool inIsIndeterminate, bool inIsHorizontal, + double inValue, double inMaxValue, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NSProgressBarCell* cell = mProgressBarCell; + + [cell setValue:inValue]; + [cell setMax:inMaxValue]; + [cell setIndeterminate:inIsIndeterminate]; + [cell setHorizontal:inIsHorizontal]; + [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [NSColor currentControlTint] + : NSClearControlTint)]; + + DrawCellWithSnapping(cell, cgContext, inBoxRect, + progressSettings[inIsHorizontal][inIsIndeterminate], + VerticalAlignFactor(aFrame), mCellDrawView, + IsFrameRTL(aFrame)); + + NS_OBJC_END_TRY_ABORT_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 + } + } +}; + +void +nsNativeThemeCocoa::DrawMeter(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK + + NS_PRECONDITION(aFrame, "aFrame should not be null here!"); + + // When using -moz-meterbar on an non meter element, we will not be able to + // get all the needed information so we just draw an empty meter. + nsIContent* content = aFrame->GetContent(); + if (!(content && content->IsHTMLElement(nsGkAtoms::meter))) { + DrawCellWithSnapping(mMeterBarCell, cgContext, inBoxRect, + meterSetting, VerticalAlignFactor(aFrame), + mCellDrawView, IsFrameRTL(aFrame)); + return; + } + + HTMLMeterElement* meterElement = static_cast<HTMLMeterElement*>(content); + double value = meterElement->Value(); + double min = meterElement->Min(); + double max = meterElement->Max(); + + NSLevelIndicatorCell* cell = mMeterBarCell; + + [cell setMinValue:min]; + [cell setMaxValue:max]; + [cell setDoubleValue: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. + */ + EventStates states = aFrame->GetContent()->AsElement()->State(); + + // Reset previously set warning and critical values. + [cell setWarningValue:max+1]; + [cell setCriticalValue:max+1]; + + if (states.HasState(NS_EVENT_STATE_SUB_OPTIMUM)) { + [cell setWarningValue:value]; + } else if (states.HasState(NS_EVENT_STATE_SUB_SUB_OPTIMUM)) { + [cell setCriticalValue:value]; + } + + HIRect rect = CGRectStandardize(inBoxRect); + BOOL vertical = IsVerticalMeter(aFrame); + + 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)); + } + + DrawCellWithSnapping(cell, cgContext, rect, + meterSetting, VerticalAlignFactor(aFrame), + mCellDrawView, !vertical && IsFrameRTL(aFrame)); + + CGContextRestoreGState(cgContext); + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +void +nsNativeThemeCocoa::DrawTabPanel(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeTabPaneDrawInfo tpdi; + + tpdi.version = 1; + tpdi.state = FrameIsInActiveWindow(aFrame) ? kThemeStateActive : kThemeStateInactive; + tpdi.direction = kThemeTabNorth; + tpdi.size = kHIThemeTabSizeNormal; + tpdi.kind = kHIThemeTabKindNormal; + + HIThemeDrawTabPane(&inBoxRect, &tpdi, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawScale(CGContextRef cgContext, const HIRect& inBoxRect, + EventStates inState, bool inIsVertical, + bool inIsReverse, int32_t inCurrentValue, + int32_t inMinValue, int32_t inMaxValue, + nsIFrame* aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeTrackDrawInfo tdi; + + tdi.version = 0; + tdi.kind = kThemeMediumSlider; + tdi.bounds = inBoxRect; + tdi.min = inMinValue; + tdi.max = inMaxValue; + tdi.value = inCurrentValue; + tdi.attributes = kThemeTrackShowThumb; + if (!inIsVertical) + tdi.attributes |= kThemeTrackHorizontal; + if (inIsReverse) + tdi.attributes |= kThemeTrackRightToLeft; + if (inState.HasState(NS_EVENT_STATE_FOCUS)) + tdi.attributes |= kThemeTrackHasFocus; + if (IsDisabled(aFrame, inState)) + tdi.enableState = kThemeTrackDisabled; + else + tdi.enableState = FrameIsInActiveWindow(aFrame) ? kThemeTrackActive : kThemeTrackInactive; + tdi.trackInfo.slider.thumbDir = kThemeThumbPlain; + tdi.trackInfo.slider.pressState = 0; + + HIThemeDrawTrack(&tdi, NULL, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_ABORT_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; +} + +CGRect +nsNativeThemeCocoa::SeparatorAdjustedRect(CGRect aRect, nsIFrame* aLeft, + nsIFrame* aCurrent, nsIFrame* aRight) +{ + // 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 (aLeft && SeparatorResponsibility(aLeft, aCurrent) == aLeft) { + // The left button draws the separator, so we need to make room for it. + aRect.origin.x += 1; + aRect.size.width -= 1; + } + if (SeparatorResponsibility(aCurrent, aRight) == aCurrent) { + // 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; + const BOOL ignoresPressedWhenSelected; + const BOOL isToolbarControl; +}; + +static const CGFloat tabHeights[3] = { 17, 20, 23 }; + +static const SegmentedControlRenderSettings tabRenderSettings = { + tabHeights, @"tab", YES, NO +}; + +static const CGFloat toolbarButtonHeights[3] = { 15, 18, 22 }; + +static const SegmentedControlRenderSettings toolbarButtonRenderSettings = { + toolbarButtonHeights, @"kCUIWidgetButtonSegmentedSCurve", NO, YES +}; + +void +nsNativeThemeCocoa::DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect, + EventStates inState, nsIFrame* aFrame, + const SegmentedControlRenderSettings& aSettings) +{ + BOOL isActive = IsActive(aFrame, aSettings.isToolbarControl); + BOOL isFocused = inState.HasState(NS_EVENT_STATE_FOCUS); + BOOL isSelected = IsSelectedButton(aFrame); + BOOL isPressed = IsPressedButton(aFrame); + if (isSelected && aSettings.ignoresPressedWhenSelected) { + isPressed = NO; + } + + BOOL isRTL = IsFrameRTL(aFrame); + nsIFrame* left = GetAdjacentSiblingFrameWithSameAppearance(aFrame, isRTL); + nsIFrame* right = GetAdjacentSiblingFrameWithSameAppearance(aFrame, !isRTL); + CGRect drawRect = SeparatorAdjustedRect(inBoxRect, left, aFrame, right); + BOOL drawLeftSeparator = SeparatorResponsibility(left, aFrame) == aFrame; + BOOL drawRightSeparator = SeparatorResponsibility(aFrame, right) == aFrame; + NSControlSize controlSize = FindControlSize(drawRect.size.height, aSettings.heights, 4.0f); + + RenderWithCoreUI(drawRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: + aSettings.widgetName, @"widget", + (isActive ? @"kCUIPresentationStateActiveKey" : @"kCUIPresentationStateInactive"), @"kCUIPresentationStateKey", + ToolbarButtonPosition(!left, !right), @"kCUIPositionKey", + [NSNumber numberWithBool:drawLeftSeparator], @"kCUISegmentLeadingSeparatorKey", + [NSNumber numberWithBool:drawRightSeparator], @"kCUISegmentTrailingSeparatorKey", + [NSNumber numberWithBool:isSelected], @"value", + (isPressed ? @"pressed" : (isActive ? @"normal" : @"inactive")), @"state", + [NSNumber numberWithBool:isFocused], @"focus", + CUIControlSizeForCocoaSize(controlSize), @"size", + [NSNumber numberWithBool:YES], @"is.flipped", + @"up", @"direction", + nil]); +} + +void +nsNativeThemeCocoa::GetScrollbarPressStates(nsIFrame* aFrame, + EventStates aButtonStates[]) +{ + static nsIContent::AttrValuesArray attributeValues[] = { + &nsGkAtoms::scrollbarUpTop, + &nsGkAtoms::scrollbarDownTop, + &nsGkAtoms::scrollbarUpBottom, + &nsGkAtoms::scrollbarDownBottom, + nullptr + }; + + // Get the state of any scrollbar buttons in our child frames + for (nsIFrame *childFrame : aFrame->PrincipalChildList()) { + nsIContent *childContent = childFrame->GetContent(); + if (!childContent) continue; + int32_t attrIndex = childContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sbattr, + attributeValues, eCaseMatters); + if (attrIndex < 0) continue; + + aButtonStates[attrIndex] = GetContentState(childFrame, NS_THEME_BUTTON); + } +} + +nsIFrame* +nsNativeThemeCocoa::GetParentScrollbarFrame(nsIFrame *aFrame) +{ + // Walk our parents to find a scrollbar frame + nsIFrame *scrollbarFrame = aFrame; + do { + if (scrollbarFrame->GetType() == nsGkAtoms::scrollbarFrame) break; + } while ((scrollbarFrame = scrollbarFrame->GetParent())); + + // We return null if we can't find a parent scrollbar frame + return scrollbarFrame; +} + +static bool +ToolbarCanBeUnified(CGContextRef cgContext, const HIRect& inBoxRect, NSWindow* aWindow) +{ + if (![aWindow isKindOfClass:[ToolbarWindow class]]) + return false; + + ToolbarWindow* win = (ToolbarWindow*)aWindow; + float unifiedToolbarHeight = [win unifiedToolbarHeight]; + return inBoxRect.origin.x == 0 && + inBoxRect.size.width >= [win frame].size.width && + CGRectGetMaxY(inBoxRect) <= unifiedToolbarHeight; +} + +// By default, kCUIWidgetWindowFrame drawing draws rounded corners in the +// upper corners. Depending on the context type, it fills the background in +// the corners with black or leaves it transparent. Unfortunately, this corner +// rounding interacts poorly with the window corner masking we apply during +// titlebar drawing and results in small remnants of the corner background +// appearing at the rounded edge. +// So we draw square corners. +static void +DrawNativeTitlebarToolbarWithSquareCorners(CGContextRef aContext, const CGRect& aRect, + CGFloat aUnifiedHeight, BOOL aIsMain, BOOL aIsFlipped) +{ + // We extend the draw rect horizontally and clip away the rounded corners. + const CGFloat extendHorizontal = 10; + CGRect drawRect = CGRectInset(aRect, -extendHorizontal, 0); + CGContextSaveGState(aContext); + CGContextClipToRect(aContext, aRect); + + RenderWithCoreUI(drawRect, aContext, + [NSDictionary dictionaryWithObjectsAndKeys: + @"kCUIWidgetWindowFrame", @"widget", + @"regularwin", @"windowtype", + (aIsMain ? @"normal" : @"inactive"), @"state", + [NSNumber numberWithDouble:aUnifiedHeight], @"kCUIWindowFrameUnifiedTitleBarHeightKey", + [NSNumber numberWithBool:YES], @"kCUIWindowFrameDrawTitleSeparatorKey", + [NSNumber numberWithBool:aIsFlipped], @"is.flipped", + nil]); + + CGContextRestoreGState(aContext); +} + +void +nsNativeThemeCocoa::DrawUnifiedToolbar(CGContextRef cgContext, const HIRect& inBoxRect, + NSWindow* aWindow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + CGContextSaveGState(cgContext); + CGContextClipToRect(cgContext, inBoxRect); + + CGFloat unifiedHeight = std::max([(ToolbarWindow*)aWindow unifiedToolbarHeight], + inBoxRect.size.height); + BOOL isMain = [aWindow isMainWindow]; + CGFloat titlebarHeight = unifiedHeight - inBoxRect.size.height; + CGRect drawRect = CGRectMake(inBoxRect.origin.x, inBoxRect.origin.y - titlebarHeight, + inBoxRect.size.width, inBoxRect.size.height + titlebarHeight); + DrawNativeTitlebarToolbarWithSquareCorners(cgContext, drawRect, unifiedHeight, isMain, YES); + + CGContextRestoreGState(cgContext); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect, + nsIFrame *aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_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", + (IsActive(aFrame, YES) ? @"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_ABORT_BLOCK; +} + +void +nsNativeThemeCocoa::DrawNativeTitlebar(CGContextRef aContext, CGRect aTitlebarRect, + CGFloat aUnifiedHeight, BOOL aIsMain, BOOL aIsFlipped) +{ + CGFloat unifiedHeight = std::max(aUnifiedHeight, aTitlebarRect.size.height); + DrawNativeTitlebarToolbarWithSquareCorners(aContext, aTitlebarRect, unifiedHeight, aIsMain, aIsFlipped); +} + +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, + nsIFrame *aFrame) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + HIThemeGrowBoxDrawInfo drawInfo; + drawInfo.version = 0; + drawInfo.state = kThemeStateActive; + drawInfo.kind = kHIThemeGrowBoxKindNormal; + drawInfo.direction = kThemeGrowRight | kThemeGrowDown; + drawInfo.size = kHIThemeGrowBoxSizeNormal; + + RenderTransformedHIThemeControl(cgContext, aRect, RenderResizer, &drawInfo, + IsFrameRTL(aFrame)); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +static void +DrawVibrancyBackground(CGContextRef cgContext, CGRect inBoxRect, + nsIFrame* aFrame, nsITheme::ThemeGeometryType aThemeGeometryType, + int aCornerRadiusIfOpaque = 0) +{ + ChildView* childView = ChildViewForFrame(aFrame); + if (childView) { + NSRect rect = NSRectFromCGRect(inBoxRect); + NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:YES]]; + [NSGraphicsContext saveGraphicsState]; + + NSColor* fillColor = [childView vibrancyFillColorForThemeGeometryType:aThemeGeometryType]; + if ([fillColor alphaComponent] == 1.0 && aCornerRadiusIfOpaque > 0) { + // The fillColor being opaque means that the system-wide pref "reduce + // transparency" is set. In that scenario, we still go through all the + // vibrancy rendering paths (VibrancyManager::SystemSupportsVibrancy() + // will still return true), but the result just won't look "vibrant". + // However, there's one unfortunate change of behavior that this pref + // has: It stops the window server from applying window masks. We use + // a window mask to get rounded corners on menus. So since the mask + // doesn't work in "reduce vibrancy" mode, we need to do our own rounded + // corner clipping here. + [[NSBezierPath bezierPathWithRoundedRect:rect + xRadius:aCornerRadiusIfOpaque + yRadius:aCornerRadiusIfOpaque] addClip]; + } + + [fillColor set]; + NSRectFill(rect); + + [NSGraphicsContext restoreGraphicsState]; + [NSGraphicsContext setCurrentContext:savedContext]; + } +} + +bool +nsNativeThemeCocoa::IsParentScrollbarRolledOver(nsIFrame* aFrame) +{ + nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); + return nsLookAndFeel::UseOverlayScrollbars() + ? CheckBooleanAttr(scrollbarFrame, nsGkAtoms::hover) + : GetContentState(scrollbarFrame, NS_THEME_NONE).HasState(NS_EVENT_STATE_HOVER); +} + +static bool +IsHiDPIContext(nsPresContext* aContext) +{ + return nsPresContext::AppUnitsPerCSSPixel() >= + 2 * aContext->DeviceContext()->AppUnitsPerDevPixelAtUnitFullZoom(); +} + +NS_IMETHODIMP +nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + const nsRect& aRect, + const nsRect& aDirtyRect) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + DrawTarget& aDrawTarget = *aContext->GetDrawTarget(); + + // setup to draw into the correct port + int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); + + gfx::Rect nativeDirtyRect = NSRectToRect(aDirtyRect, p2a); + gfxRect nativeWidgetRect(aRect.x, aRect.y, aRect.width, aRect.height); + nativeWidgetRect.ScaleInverse(gfxFloat(p2a)); + float nativeWidgetHeight = round(nativeWidgetRect.Height()); + nativeWidgetRect.Round(); + if (nativeWidgetRect.IsEmpty()) + return NS_OK; // Don't attempt to draw invisible widgets. + + AutoRestoreTransform autoRestoreTransform(&aDrawTarget); + + bool hidpi = IsHiDPIContext(aFrame->PresContext()); + if (hidpi) { + // Use high-resolution drawing. + nativeWidgetRect.Scale(0.5f); + nativeWidgetHeight *= 0.5f; + nativeDirtyRect.Scale(0.5f); + aDrawTarget.SetTransform(aDrawTarget.GetTransform().PreScale(2.0f, 2.0f)); + } + + gfxQuartzNativeDrawing nativeDrawing(aDrawTarget, nativeDirtyRect); + + 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 NS_OK; + } + + if (hidpi) { + // Set the context's "base transform" to in order to get correctly-sized focus rings. + CGContextSetBaseCTM(cgContext, CGAffineTransformMakeScale(2, 2)); + } + +#if 0 + if (1 /*aWidgetType == NS_THEME_TEXTFIELD*/) { + fprintf(stderr, "Native theme drawing widget %d [%p] dis:%d in rect [%d %d %d %d]\n", + aWidgetType, aFrame, IsDisabled(aFrame), aRect.x, aRect.y, aRect.width, aRect.height); + fprintf(stderr, "Cairo matrix: [%f %f %f %f %f %f]\n", + mat._11, mat._12, mat._21, mat._22, mat._31, mat._32); + fprintf(stderr, "Native theme xform[0]: [%f %f %f %f %f %f]\n", + mm0.a, mm0.b, mm0.c, mm0.d, mm0.tx, mm0.ty); + CGAffineTransform mm = CGContextGetCTM(cgContext); + fprintf(stderr, "Native theme xform[1]: [%f %f %f %f %f %f]\n", + mm.a, mm.b, mm.c, mm.d, mm.tx, mm.ty); + } +#endif + + CGRect macRect = CGRectMake(nativeWidgetRect.X(), nativeWidgetRect.Y(), + nativeWidgetRect.Width(), nativeWidgetRect.Height()); + +#if 0 + fprintf(stderr, " --> macRect %f %f %f %f\n", + macRect.origin.x, macRect.origin.y, macRect.size.width, macRect.size.height); + CGRect bounds = CGContextGetClipBoundingBox(cgContext); + fprintf(stderr, " --> clip bounds: %f %f %f %f\n", + bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height); + + //CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 1.0, 0.1); + //CGContextFillRect(cgContext, bounds); +#endif + + EventStates eventState = GetContentState(aFrame, aWidgetType); + + switch (aWidgetType) { + case NS_THEME_DIALOG: { + if (IsWindowSheet(aFrame)) { + if (VibrancyManager::SystemSupportsVibrancy()) { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + DrawVibrancyBackground(cgContext, macRect, aFrame, type); + } else { + HIThemeSetFill(kThemeBrushSheetBackgroundTransparent, NULL, cgContext, HITHEME_ORIENTATION); + CGContextFillRect(cgContext, macRect); + } + } else { + HIThemeSetFill(kThemeBrushDialogBackgroundActive, NULL, cgContext, HITHEME_ORIENTATION); + CGContextFillRect(cgContext, macRect); + } + + } + break; + + case NS_THEME_MENUPOPUP: + if (VibrancyManager::SystemSupportsVibrancy()) { + DrawVibrancyBackground(cgContext, macRect, aFrame, eThemeGeometryTypeMenu, 4); + } else { + HIThemeMenuDrawInfo mdi; + memset(&mdi, 0, sizeof(mdi)); + mdi.version = 0; + mdi.menuType = IsDisabled(aFrame, eventState) ? + static_cast<ThemeMenuType>(kThemeMenuTypeInactive) : + static_cast<ThemeMenuType>(kThemeMenuTypePopUp); + + bool isLeftOfParent = false; + if (IsSubmenu(aFrame, &isLeftOfParent) && !isLeftOfParent) { + mdi.menuType = kThemeMenuTypeHierarchical; + } + + // The rounded corners draw outside the frame. + CGRect deflatedRect = CGRectMake(macRect.origin.x, macRect.origin.y + 4, + macRect.size.width, macRect.size.height - 8); + HIThemeDrawMenuBackground(&deflatedRect, &mdi, cgContext, HITHEME_ORIENTATION); + } + break; + + case NS_THEME_MENUARROW: { + bool isRTL = IsFrameRTL(aFrame); + DrawMenuIcon(cgContext, macRect, eventState, aFrame, kMenuarrowSize, + isRTL ? kMenuarrowLeftImage : kMenuarrowRightImage, true); + } + break; + + case NS_THEME_MENUITEM: + case NS_THEME_CHECKMENUITEM: { + if (VibrancyManager::SystemSupportsVibrancy()) { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + DrawVibrancyBackground(cgContext, macRect, aFrame, type); + } else { + bool isDisabled = IsDisabled(aFrame, eventState); + bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + // maybe use kThemeMenuItemHierBackground or PopUpBackground instead of just Plain? + HIThemeMenuItemDrawInfo drawInfo; + memset(&drawInfo, 0, sizeof(drawInfo)); + drawInfo.version = 0; + drawInfo.itemType = kThemeMenuItemPlain; + drawInfo.state = (isDisabled ? + static_cast<ThemeMenuState>(kThemeMenuDisabled) : + isSelected ? + static_cast<ThemeMenuState>(kThemeMenuSelected) : + static_cast<ThemeMenuState>(kThemeMenuActive)); + + // XXX pass in the menu rect instead of always using the item rect + HIRect ignored; + HIThemeDrawMenuItem(&macRect, &macRect, &drawInfo, cgContext, HITHEME_ORIENTATION, &ignored); + } + + if (aWidgetType == NS_THEME_CHECKMENUITEM) { + DrawMenuIcon(cgContext, macRect, eventState, aFrame, kCheckmarkSize, kCheckmarkImage, false); + } + } + break; + + case NS_THEME_MENUSEPARATOR: { + ThemeMenuState menuState; + if (IsDisabled(aFrame, eventState)) { + menuState = kThemeMenuDisabled; + } + else { + menuState = CheckBooleanAttr(aFrame, nsGkAtoms::menuactive) ? + kThemeMenuSelected : kThemeMenuActive; + } + + HIThemeMenuItemDrawInfo midi = { 0, kThemeMenuItemPlain, menuState }; + HIThemeDrawMenuSeparator(&macRect, &macRect, &midi, cgContext, HITHEME_ORIENTATION); + } + break; + + case NS_THEME_BUTTON_ARROW_UP: + case NS_THEME_BUTTON_ARROW_DOWN: + DrawMenuIcon(cgContext, macRect, eventState, aFrame, kMenuScrollArrowSize, + aWidgetType == NS_THEME_BUTTON_ARROW_UP ? + kMenuUpScrollArrowImage : kMenuDownScrollArrowImage, true); + break; + + case NS_THEME_TOOLTIP: + if (VibrancyManager::SystemSupportsVibrancy()) { + DrawVibrancyBackground(cgContext, macRect, aFrame, ThemeGeometryTypeForWidget(aFrame, aWidgetType)); + } else { + CGContextSetRGBFillColor(cgContext, 0.996, 1.000, 0.792, 0.950); + CGContextFillRect(cgContext, macRect); + } + break; + + case NS_THEME_CHECKBOX: + case NS_THEME_RADIO: { + bool isCheckbox = (aWidgetType == NS_THEME_CHECKBOX); + DrawCheckboxOrRadio(cgContext, isCheckbox, macRect, GetCheckedOrSelected(aFrame, !isCheckbox), + eventState, aFrame); + } + break; + + case NS_THEME_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.) + EventStates docState = aFrame->GetContent()->OwnerDoc()->GetDocumentState(); + bool isInActiveWindow = !docState.HasState(NS_DOCUMENT_STATE_WINDOW_INACTIVE); + if (!IsDisabled(aFrame, eventState) && isInActiveWindow && + !QueueAnimatedContentForRefresh(aFrame->GetContent(), 10)) { + NS_WARNING("Unable to animate button!"); + } + DrawButton(cgContext, kThemePushButton, macRect, isInActiveWindow, + kThemeButtonOff, kThemeAdornmentNone, eventState, aFrame); + } else if (IsButtonTypeMenu(aFrame)) { + DrawDropdown(cgContext, macRect, eventState, aWidgetType, aFrame); + } else { + DrawPushButton(cgContext, macRect, eventState, aWidgetType, aFrame, + nativeWidgetHeight); + } + break; + + case NS_THEME_FOCUS_OUTLINE: + DrawFocusOutline(cgContext, macRect, eventState, aWidgetType, aFrame); + break; + + case NS_THEME_MAC_HELP_BUTTON: + case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: + case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: + DrawPushButton(cgContext, macRect, eventState, aWidgetType, aFrame, + nativeWidgetHeight); + break; + + case NS_THEME_BUTTON_BEVEL: + DrawButton(cgContext, kThemeMediumBevelButton, macRect, + IsDefaultButton(aFrame), kThemeButtonOff, kThemeAdornmentNone, + eventState, aFrame); + break; + + case NS_THEME_SPINNER: { + nsIContent* content = aFrame->GetContent(); + if (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; + } + ThemeDrawState state = kThemeStateActive; + if (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, + NS_LITERAL_STRING("up"), eCaseMatters)) { + state = kThemeStatePressedUp; + } + else if (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, + NS_LITERAL_STRING("down"), eCaseMatters)) { + state = kThemeStatePressedDown; + } + + DrawSpinButtons(cgContext, kThemeIncDecButton, macRect, state, + kThemeAdornmentNone, eventState, aFrame); + } + break; + + case NS_THEME_SPINNER_UPBUTTON: + case NS_THEME_SPINNER_DOWNBUTTON: { + nsNumberControlFrame* numberControlFrame = + nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame); + if (numberControlFrame) { + ThemeDrawState state = kThemeStateActive; + if (numberControlFrame->SpinnerUpButtonIsDepressed()) { + state = kThemeStatePressedUp; + } else if (numberControlFrame->SpinnerDownButtonIsDepressed()) { + state = kThemeStatePressedDown; + } + DrawSpinButton(cgContext, kThemeIncDecButtonMini, macRect, state, + kThemeAdornmentNone, eventState, aFrame, aWidgetType); + } + } + break; + + case NS_THEME_TOOLBARBUTTON: + DrawSegment(cgContext, macRect, eventState, aFrame, toolbarButtonRenderSettings); + break; + + case NS_THEME_SEPARATOR: { + HIThemeSeparatorDrawInfo sdi = { 0, kThemeStateActive }; + HIThemeDrawSeparator(&macRect, &sdi, cgContext, HITHEME_ORIENTATION); + } + break; + + case NS_THEME_TOOLBAR: { + NSWindow* win = NativeWindowForFrame(aFrame); + if (ToolbarCanBeUnified(cgContext, macRect, win)) { + DrawUnifiedToolbar(cgContext, macRect, win); + break; + } + BOOL isMain = [win isMainWindow]; + CGRect drawRect = macRect; + + // top border + drawRect.size.height = 1.0f; + DrawNativeGreyColorInRect(cgContext, toolbarTopBorderGrey, drawRect, isMain); + + // background + drawRect.origin.y += drawRect.size.height; + drawRect.size.height = macRect.size.height - 2.0f; + DrawNativeGreyColorInRect(cgContext, toolbarFillGrey, drawRect, isMain); + + // bottom border + drawRect.origin.y += drawRect.size.height; + drawRect.size.height = 1.0f; + DrawNativeGreyColorInRect(cgContext, toolbarBottomBorderGrey, drawRect, isMain); + } + break; + + case NS_THEME_WINDOW_TITLEBAR: { + NSWindow* win = NativeWindowForFrame(aFrame); + BOOL isMain = [win isMainWindow]; + float unifiedToolbarHeight = [win isKindOfClass:[ToolbarWindow class]] ? + [(ToolbarWindow*)win unifiedToolbarHeight] : macRect.size.height; + DrawNativeTitlebar(cgContext, macRect, unifiedToolbarHeight, isMain, YES); + } + break; + + case NS_THEME_STATUSBAR: + DrawStatusBar(cgContext, macRect, aFrame); + break; + + case NS_THEME_MENULIST: + case NS_THEME_MENULIST_TEXTFIELD: + DrawDropdown(cgContext, macRect, eventState, aWidgetType, aFrame); + break; + + case NS_THEME_MENULIST_BUTTON: + DrawButton(cgContext, kThemeArrowButton, macRect, false, kThemeButtonOn, + kThemeAdornmentArrowDownArrow, eventState, aFrame); + break; + + case NS_THEME_GROUPBOX: { + HIThemeGroupBoxDrawInfo gdi = { 0, kThemeStateActive, kHIThemeGroupBoxKindPrimary }; + HIThemeDrawGroupBox(&macRect, &gdi, cgContext, HITHEME_ORIENTATION); + break; + } + + case NS_THEME_TEXTFIELD: + case NS_THEME_NUMBER_INPUT: + // HIThemeSetFill is not available on 10.3 + CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); + CGContextFillRect(cgContext, macRect); + + // XUL textboxes set the native appearance on the containing box, while + // concrete focus is set on the html:input element within it. We can + // though, check the focused attribute of xul textboxes in this case. + // On Mac, focus rings are always shown for textboxes, so we do not need + // to check the window's focus ring state here + if (aFrame->GetContent()->IsXULElement() && IsFocused(aFrame)) { + eventState |= NS_EVENT_STATE_FOCUS; + } + + DrawFrame(cgContext, kHIThemeFrameTextFieldSquare, macRect, + IsDisabled(aFrame, eventState) || IsReadOnly(aFrame), eventState); + break; + + case NS_THEME_SEARCHFIELD: + DrawSearchField(cgContext, macRect, aFrame, eventState); + break; + + case NS_THEME_PROGRESSBAR: + { + double value = GetProgressValue(aFrame); + double maxValue = GetProgressMaxValue(aFrame); + // Don't request repaints for scrollbars at 100% because those don't animate. + if (value < maxValue) { + if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) { + NS_WARNING("Unable to animate progressbar!"); + } + } + DrawProgress(cgContext, macRect, IsIndeterminateProgress(aFrame, eventState), + !IsVerticalProgress(aFrame), + value, maxValue, aFrame); + break; + } + + case NS_THEME_PROGRESSBAR_VERTICAL: + DrawProgress(cgContext, macRect, IsIndeterminateProgress(aFrame, eventState), + false, GetProgressValue(aFrame), + GetProgressMaxValue(aFrame), aFrame); + break; + + case NS_THEME_METERBAR: + DrawMeter(cgContext, macRect, aFrame); + break; + + case NS_THEME_PROGRESSCHUNK: + case NS_THEME_PROGRESSCHUNK_VERTICAL: + case NS_THEME_METERCHUNK: + // Do nothing: progress and meter bars cases will draw chunks. + break; + + case NS_THEME_TREETWISTY: + DrawButton(cgContext, kThemeDisclosureButton, macRect, false, + kThemeDisclosureRight, kThemeAdornmentNone, eventState, aFrame); + break; + + case NS_THEME_TREETWISTYOPEN: + DrawButton(cgContext, kThemeDisclosureButton, macRect, false, + kThemeDisclosureDown, kThemeAdornmentNone, eventState, aFrame); + break; + + case NS_THEME_TREEHEADERCELL: { + TreeSortDirection sortDirection = GetTreeSortDirection(aFrame); + DrawButton(cgContext, kThemeListHeaderButton, macRect, false, + sortDirection == eTreeSortDirection_Natural ? kThemeButtonOff : kThemeButtonOn, + sortDirection == eTreeSortDirection_Ascending ? + kThemeAdornmentHeaderButtonSortUp : kThemeAdornmentNone, eventState, aFrame); + } + break; + + case NS_THEME_TREEITEM: + case NS_THEME_TREEVIEW: + // HIThemeSetFill is not available on 10.3 + // HIThemeSetFill(kThemeBrushWhite, NULL, cgContext, HITHEME_ORIENTATION); + CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); + CGContextFillRect(cgContext, macRect); + break; + + case NS_THEME_TREEHEADER: + // do nothing, taken care of by individual header cells + case NS_THEME_TREEHEADERSORTARROW: + // do nothing, taken care of by treeview header + case NS_THEME_TREELINE: + // do nothing, these lines don't exist on macos + break; + + case NS_THEME_SCALE_HORIZONTAL: + case NS_THEME_SCALE_VERTICAL: { + int32_t curpos = CheckIntAttr(aFrame, nsGkAtoms::curpos, 0); + int32_t minpos = CheckIntAttr(aFrame, nsGkAtoms::minpos, 0); + int32_t maxpos = CheckIntAttr(aFrame, nsGkAtoms::maxpos, 100); + if (!maxpos) + maxpos = 100; + + bool reverse = aFrame->GetContent()-> + AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir, + NS_LITERAL_STRING("reverse"), eCaseMatters); + DrawScale(cgContext, macRect, eventState, + (aWidgetType == NS_THEME_SCALE_VERTICAL), reverse, + curpos, minpos, maxpos, aFrame); + } + break; + + case NS_THEME_SCALETHUMB_HORIZONTAL: + case NS_THEME_SCALETHUMB_VERTICAL: + // do nothing, drawn by scale + break; + + case NS_THEME_RANGE: { + nsRangeFrame *rangeFrame = do_QueryFrame(aFrame); + if (!rangeFrame) { + break; + } + // DrawScale requires integer min, max and value. This is purely for + // drawing, so we normalize to a range 0-1000 here. + int32_t value = int32_t(rangeFrame->GetValueAsFractionOfRange() * 1000); + int32_t min = 0; + int32_t max = 1000; + bool isVertical = !IsRangeHorizontal(aFrame); + bool reverseDir = isVertical || rangeFrame->IsRightToLeft(); + DrawScale(cgContext, macRect, eventState, isVertical, reverseDir, + value, min, max, aFrame); + break; + } + + case NS_THEME_SCROLLBAR_SMALL: + case NS_THEME_SCROLLBAR: + break; + case NS_THEME_SCROLLBARTHUMB_VERTICAL: + case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: { + BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars(); + BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTHUMB_HORIZONTAL); + BOOL isRolledOver = IsParentScrollbarRolledOver(aFrame); + nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); + bool isSmall = (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); + if (isOverlay && !isRolledOver) { + if (isHorizontal) { + macRect.origin.y += 4; + macRect.size.height -= 4; + } else { + if (aFrame->StyleVisibility()->mDirection != + NS_STYLE_DIRECTION_RTL) { + macRect.origin.x += 4; + } + macRect.size.width -= 4; + } + } + const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame); + NSMutableDictionary* options = [NSMutableDictionary dictionaryWithObjectsAndKeys: + (isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget", + (isSmall ? @"small" : @"regular"), @"size", + (isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey", + (isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey", + [NSNumber numberWithBool:YES], @"indiconly", + [NSNumber numberWithBool:YES], @"kCUIThumbProportionKey", + [NSNumber numberWithBool:YES], @"is.flipped", + nil]; + if (isRolledOver) { + [options setObject:@"rollover" forKey:@"state"]; + } + RenderWithCoreUI(macRect, cgContext, options, true); + } + break; + + case NS_THEME_SCROLLBARBUTTON_UP: + case NS_THEME_SCROLLBARBUTTON_LEFT: +#if SCROLLBARS_VISUAL_DEBUG + CGContextSetRGBFillColor(cgContext, 1.0, 0, 0, 0.6); + CGContextFillRect(cgContext, macRect); +#endif + break; + case NS_THEME_SCROLLBARBUTTON_DOWN: + case NS_THEME_SCROLLBARBUTTON_RIGHT: +#if SCROLLBARS_VISUAL_DEBUG + CGContextSetRGBFillColor(cgContext, 0, 1.0, 0, 0.6); + CGContextFillRect(cgContext, macRect); +#endif + break; + case NS_THEME_SCROLLBARTRACK_HORIZONTAL: + case NS_THEME_SCROLLBARTRACK_VERTICAL: { + BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars(); + if (!isOverlay || IsParentScrollbarRolledOver(aFrame)) { + BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTRACK_HORIZONTAL); + nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); + bool isSmall = (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); + const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame); + RenderWithCoreUI(macRect, cgContext, + [NSDictionary dictionaryWithObjectsAndKeys: + (isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget", + (isSmall ? @"small" : @"regular"), @"size", + (isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey", + (isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey", + [NSNumber numberWithBool:YES], @"noindicator", + [NSNumber numberWithBool:YES], @"kCUIThumbProportionKey", + [NSNumber numberWithBool:YES], @"is.flipped", + nil], + true); + } + } + break; + + case NS_THEME_TEXTFIELD_MULTILINE: { + // we have to draw this by hand because there is no HITheme value for it + CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); + + CGContextFillRect(cgContext, macRect); + + // #737373 for the top border, #999999 for the rest. + float x = macRect.origin.x, y = macRect.origin.y; + float w = macRect.size.width, h = macRect.size.height; + CGContextSetRGBFillColor(cgContext, 0.4510, 0.4510, 0.4510, 1.0); + CGContextFillRect(cgContext, CGRectMake(x, y, w, 1)); + CGContextSetRGBFillColor(cgContext, 0.6, 0.6, 0.6, 1.0); + CGContextFillRect(cgContext, CGRectMake(x, y + 1, 1, h - 1)); + CGContextFillRect(cgContext, CGRectMake(x + w - 1, y + 1, 1, h - 1)); + CGContextFillRect(cgContext, CGRectMake(x + 1, y + h - 1, w - 2, 1)); + + // draw a focus ring + if (eventState.HasState(NS_EVENT_STATE_FOCUS)) { + NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:YES]]; + CGContextSaveGState(cgContext); + NSSetFocusRingStyle(NSFocusRingOnly); + NSRectFill(NSRectFromCGRect(macRect)); + CGContextRestoreGState(cgContext); + [NSGraphicsContext setCurrentContext:savedContext]; + } + } + break; + + case NS_THEME_LISTBOX: { + // We have to draw this by hand because kHIThemeFrameListBox drawing + // is buggy on 10.5, see bug 579259. + CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); + CGContextFillRect(cgContext, macRect); + + // #8E8E8E for the top border, #BEBEBE for the rest. + float x = macRect.origin.x, y = macRect.origin.y; + float w = macRect.size.width, h = macRect.size.height; + CGContextSetRGBFillColor(cgContext, 0.557, 0.557, 0.557, 1.0); + CGContextFillRect(cgContext, CGRectMake(x, y, w, 1)); + CGContextSetRGBFillColor(cgContext, 0.745, 0.745, 0.745, 1.0); + CGContextFillRect(cgContext, CGRectMake(x, y + 1, 1, h - 1)); + CGContextFillRect(cgContext, CGRectMake(x + w - 1, y + 1, 1, h - 1)); + CGContextFillRect(cgContext, CGRectMake(x + 1, y + h - 1, w - 2, 1)); + } + break; + + case NS_THEME_MAC_SOURCE_LIST: { + if (VibrancyManager::SystemSupportsVibrancy()) { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + DrawVibrancyBackground(cgContext, macRect, aFrame, type); + } else { + CGGradientRef backgroundGradient; + CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); + CGFloat activeGradientColors[8] = { 0.9137, 0.9294, 0.9490, 1.0, + 0.8196, 0.8471, 0.8784, 1.0 }; + CGFloat inactiveGradientColors[8] = { 0.9686, 0.9686, 0.9686, 1.0, + 0.9216, 0.9216, 0.9216, 1.0 }; + CGPoint start = macRect.origin; + CGPoint end = CGPointMake(macRect.origin.x, + macRect.origin.y + macRect.size.height); + BOOL isActive = FrameIsInActiveWindow(aFrame); + backgroundGradient = + CGGradientCreateWithColorComponents(rgb, isActive ? activeGradientColors + : inactiveGradientColors, NULL, 2); + CGContextDrawLinearGradient(cgContext, backgroundGradient, start, end, 0); + CGGradientRelease(backgroundGradient); + CGColorSpaceRelease(rgb); + } + } + break; + + case NS_THEME_MAC_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: { + // If we're in XUL tree, we need to rely on the source list's clear + // background display item. If we cleared the background behind the + // selections, the source list would not pick up the right font + // smoothing background. So, to simplify a bit, we only support vibrancy + // if we're in a source list. + if (VibrancyManager::SystemSupportsVibrancy() && IsInSourceList(aFrame)) { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + DrawVibrancyBackground(cgContext, macRect, aFrame, type); + } else { + BOOL isActiveSelection = + aWidgetType == NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION; + RenderWithCoreUI(macRect, cgContext, + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:isActiveSelection], @"focus", + [NSNumber numberWithBool:YES], @"is.flipped", + @"kCUIVariantGradientSideBarSelection", @"kCUIVariantKey", + (FrameIsInActiveWindow(aFrame) ? @"normal" : @"inactive"), @"state", + @"gradient", @"widget", + nil]); + } + } + break; + + case NS_THEME_TAB: + DrawSegment(cgContext, macRect, eventState, aFrame, tabRenderSettings); + break; + + case NS_THEME_TABPANELS: + DrawTabPanel(cgContext, macRect, aFrame); + break; + + case NS_THEME_RESIZER: + DrawResizer(cgContext, macRect, aFrame); + break; + + case NS_THEME_MAC_VIBRANCY_LIGHT: + case NS_THEME_MAC_VIBRANCY_DARK: { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + DrawVibrancyBackground(cgContext, macRect, aFrame, type); + break; + } + } + + if (hidpi) { + // Reset the base CTM. + CGContextSetBaseCTM(cgContext, CGAffineTransformIdentity); + } + + nativeDrawing.EndNativeDrawing(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsIntMargin +nsNativeThemeCocoa::DirectionAwareMargin(const nsIntMargin& 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 nsIntMargin(m.top, m.right, m.bottom, m.left); +} + +static const nsIntMargin kAquaDropdownBorder(1, 22, 2, 5); +static const nsIntMargin kAquaComboboxBorder(3, 20, 3, 4); +static const nsIntMargin kAquaSearchfieldBorder(3, 5, 2, 19); + +NS_IMETHODIMP +nsNativeThemeCocoa::GetWidgetBorder(nsDeviceContext* aContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + nsIntMargin* aResult) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + aResult->SizeTo(0, 0, 0, 0); + + switch (aWidgetType) { + case NS_THEME_BUTTON: + { + if (IsButtonTypeMenu(aFrame)) { + *aResult = DirectionAwareMargin(kAquaDropdownBorder, aFrame); + } else { + *aResult = DirectionAwareMargin(nsIntMargin(1, 7, 3, 7), aFrame); + } + break; + } + + case NS_THEME_TOOLBARBUTTON: + { + *aResult = DirectionAwareMargin(nsIntMargin(1, 4, 1, 4), aFrame); + break; + } + + case NS_THEME_CHECKBOX: + case NS_THEME_RADIO: + { + // nsFormControlFrame::GetIntrinsicWidth and nsFormControlFrame::GetIntrinsicHeight + // assume a border width of 2px. + aResult->SizeTo(2, 2, 2, 2); + break; + } + + case NS_THEME_MENULIST: + case NS_THEME_MENULIST_BUTTON: + *aResult = DirectionAwareMargin(kAquaDropdownBorder, aFrame); + break; + + case NS_THEME_MENULIST_TEXTFIELD: + *aResult = DirectionAwareMargin(kAquaComboboxBorder, aFrame); + break; + + case NS_THEME_NUMBER_INPUT: + case NS_THEME_TEXTFIELD: + { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset); + + SInt32 textPadding = 0; + ::GetThemeMetric(kThemeMetricEditTextWhitespace, &textPadding); + + frameOutset += textPadding; + + aResult->SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); + break; + } + + case NS_THEME_TEXTFIELD_MULTILINE: + aResult->SizeTo(1, 1, 1, 1); + break; + + case NS_THEME_SEARCHFIELD: + *aResult = DirectionAwareMargin(kAquaSearchfieldBorder, aFrame); + break; + + case NS_THEME_LISTBOX: + { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset); + aResult->SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); + break; + } + + case NS_THEME_SCROLLBARTRACK_HORIZONTAL: + case NS_THEME_SCROLLBARTRACK_VERTICAL: + { + bool isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTRACK_HORIZONTAL); + if (nsLookAndFeel::UseOverlayScrollbars()) { + if (!nsCocoaFeatures::OnYosemiteOrLater()) { + // Pre-10.10, we have to center the thumb rect in the middle of the + // scrollbar. Starting with 10.10, the expected rect for thumb + // rendering is the full width of the scrollbar. + if (isHorizontal) { + aResult->top = 2; + aResult->bottom = 1; + } else { + aResult->left = 2; + aResult->right = 1; + } + } + // Leave a bit of space at the start and the end on all OS X versions. + if (isHorizontal) { + aResult->left = 1; + aResult->right = 1; + } else { + aResult->top = 1; + aResult->bottom = 1; + } + } + + break; + } + + case NS_THEME_STATUSBAR: + aResult->SizeTo(1, 0, 0, 0); + break; + } + + if (IsHiDPIContext(aFrame->PresContext())) { + *aResult = *aResult + *aResult; // doubled + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// 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, + uint8_t aWidgetType, + nsIntMargin* aResult) +{ + // We don't want CSS padding being used for certain widgets. + // See bug 381639 for an example of why. + switch (aWidgetType) { + // Radios and checkboxes return a fixed size in GetMinimumWidgetSize + // and have a meaningful baseline, so they can't have + // author-specified padding. + case NS_THEME_CHECKBOX: + case NS_THEME_RADIO: + aResult->SizeTo(0, 0, 0, 0); + return true; + } + return false; +} + +bool +nsNativeThemeCocoa::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + uint8_t aWidgetType, nsRect* aOverflowRect) +{ + int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); + switch (aWidgetType) { + case NS_THEME_BUTTON: + case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: + case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: + case NS_THEME_MAC_HELP_BUTTON: + case NS_THEME_TOOLBARBUTTON: + case NS_THEME_NUMBER_INPUT: + case NS_THEME_TEXTFIELD: + case NS_THEME_TEXTFIELD_MULTILINE: + case NS_THEME_SEARCHFIELD: + case NS_THEME_LISTBOX: + case NS_THEME_MENULIST: + case NS_THEME_MENULIST_BUTTON: + case NS_THEME_MENULIST_TEXTFIELD: + case NS_THEME_CHECKBOX: + case NS_THEME_RADIO: + case NS_THEME_TAB: + { + // We assume that the above widgets can draw a focus ring that will be less than + // or equal to 4 pixels thick. + nsIntMargin extraSize = nsIntMargin(kMaxFocusRingWidth, + kMaxFocusRingWidth, + kMaxFocusRingWidth, + kMaxFocusRingWidth); + nsMargin m(NSIntPixelsToAppUnits(extraSize.top, p2a), + NSIntPixelsToAppUnits(extraSize.right, p2a), + NSIntPixelsToAppUnits(extraSize.bottom, p2a), + NSIntPixelsToAppUnits(extraSize.left, p2a)); + aOverflowRect->Inflate(m); + return true; + } + case NS_THEME_PROGRESSBAR: + { + // Progress bars draw a 2 pixel white shadow under their progress indicators + nsMargin m(0, 0, NSIntPixelsToAppUnits(2, p2a), 0); + aOverflowRect->Inflate(m); + return true; + } + case NS_THEME_FOCUS_OUTLINE: + { + aOverflowRect->Inflate(NSIntPixelsToAppUnits(2, p2a)); + return true; + } + } + + return false; +} + +static const int32_t kRegularScrollbarThumbMinSize = 26; +static const int32_t kSmallScrollbarThumbMinSize = 26; + +NS_IMETHODIMP +nsNativeThemeCocoa::GetMinimumWidgetSize(nsPresContext* aPresContext, + nsIFrame* aFrame, + uint8_t aWidgetType, + LayoutDeviceIntSize* aResult, + bool* aIsOverridable) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + aResult->SizeTo(0,0); + *aIsOverridable = true; + + switch (aWidgetType) { + case NS_THEME_BUTTON: + { + aResult->SizeTo(pushButtonSettings.minimumSizes[miniControlSize].width, + pushButtonSettings.naturalSizes[miniControlSize].height); + break; + } + + case NS_THEME_BUTTON_ARROW_UP: + case NS_THEME_BUTTON_ARROW_DOWN: + { + aResult->SizeTo(kMenuScrollArrowSize.width, kMenuScrollArrowSize.height); + *aIsOverridable = false; + break; + } + + case NS_THEME_MENUARROW: + { + aResult->SizeTo(kMenuarrowSize.width, kMenuarrowSize.height); + *aIsOverridable = false; + break; + } + + case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: + case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: + { + aResult->SizeTo(kDisclosureButtonSize.width, kDisclosureButtonSize.height); + *aIsOverridable = false; + break; + } + + case NS_THEME_MAC_HELP_BUTTON: + { + aResult->SizeTo(kHelpButtonSize.width, kHelpButtonSize.height); + *aIsOverridable = false; + break; + } + + case NS_THEME_TOOLBARBUTTON: + { + aResult->SizeTo(0, toolbarButtonHeights[miniControlSize]); + break; + } + + case NS_THEME_SPINNER: + case NS_THEME_SPINNER_UPBUTTON: + case NS_THEME_SPINNER_DOWNBUTTON: + { + SInt32 buttonHeight = 0, buttonWidth = 0; + if (aFrame->GetContent()->IsXULElement()) { + ::GetThemeMetric(kThemeMetricLittleArrowsWidth, &buttonWidth); + ::GetThemeMetric(kThemeMetricLittleArrowsHeight, &buttonHeight); + } else { + NSSize size = + spinnerSettings.minimumSizes[EnumSizeForCocoaSize(NSMiniControlSize)]; + buttonWidth = size.width; + buttonHeight = size.height; + if (aWidgetType != NS_THEME_SPINNER) { + // the buttons are half the height of the spinner + buttonHeight /= 2; + } + } + aResult->SizeTo(buttonWidth, buttonHeight); + *aIsOverridable = true; + break; + } + + case NS_THEME_MENULIST: + case NS_THEME_MENULIST_BUTTON: + { + SInt32 popupHeight = 0; + ::GetThemeMetric(kThemeMetricPopupButtonHeight, &popupHeight); + aResult->SizeTo(0, popupHeight); + break; + } + + case NS_THEME_NUMBER_INPUT: + case NS_THEME_TEXTFIELD: + case NS_THEME_TEXTFIELD_MULTILINE: + case NS_THEME_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. + aResult->SizeTo(0, (2 + 2) /* top */ + 9 + (1 + 1) /* bottom */); + break; + } + + case NS_THEME_WINDOW_BUTTON_BOX: { + NSSize size = WindowButtonsSize(aFrame); + aResult->SizeTo(size.width, size.height); + *aIsOverridable = false; + break; + } + + case NS_THEME_MAC_FULLSCREEN_BUTTON: { + if ([NativeWindowForFrame(aFrame) respondsToSelector:@selector(toggleFullScreen:)] && + !nsCocoaFeatures::OnYosemiteOrLater()) { + // This value is hardcoded because it's needed before we can measure the + // position and size of the fullscreen button. + aResult->SizeTo(16, 17); + } + *aIsOverridable = false; + break; + } + + case NS_THEME_PROGRESSBAR: + { + SInt32 barHeight = 0; + ::GetThemeMetric(kThemeMetricNormalProgressBarThickness, &barHeight); + aResult->SizeTo(0, barHeight); + break; + } + + case NS_THEME_TREETWISTY: + case NS_THEME_TREETWISTYOPEN: + { + SInt32 twistyHeight = 0, twistyWidth = 0; + ::GetThemeMetric(kThemeMetricDisclosureButtonWidth, &twistyWidth); + ::GetThemeMetric(kThemeMetricDisclosureButtonHeight, &twistyHeight); + aResult->SizeTo(twistyWidth, twistyHeight); + *aIsOverridable = false; + break; + } + + case NS_THEME_TREEHEADER: + case NS_THEME_TREEHEADERCELL: + { + SInt32 headerHeight = 0; + ::GetThemeMetric(kThemeMetricListHeaderHeight, &headerHeight); + aResult->SizeTo(0, headerHeight - 1); // We don't need the top border. + break; + } + + case NS_THEME_TAB: + { + aResult->SizeTo(0, tabHeights[miniControlSize]); + break; + } + + case NS_THEME_RANGE: + { + // The Mac Appearance Manager API (the old API we're currently using) + // doesn't define constants to obtain a minimum size for sliders. We use + // the "thickness" of a slider that has default dimensions for both the + // minimum width and height to get something sane and so that paint + // invalidation works. + SInt32 size = 0; + if (IsRangeHorizontal(aFrame)) { + ::GetThemeMetric(kThemeMetricHSliderHeight, &size); + } else { + ::GetThemeMetric(kThemeMetricVSliderWidth, &size); + } + aResult->SizeTo(size, size); + *aIsOverridable = true; + break; + } + + case NS_THEME_RANGE_THUMB: + { + SInt32 width = 0; + SInt32 height = 0; + ::GetThemeMetric(kThemeMetricSliderMinThumbWidth, &width); + ::GetThemeMetric(kThemeMetricSliderMinThumbHeight, &height); + aResult->SizeTo(width, height); + *aIsOverridable = false; + break; + } + + case NS_THEME_SCALE_HORIZONTAL: + { + SInt32 scaleHeight = 0; + ::GetThemeMetric(kThemeMetricHSliderHeight, &scaleHeight); + aResult->SizeTo(scaleHeight, scaleHeight); + *aIsOverridable = false; + break; + } + + case NS_THEME_SCALE_VERTICAL: + { + SInt32 scaleWidth = 0; + ::GetThemeMetric(kThemeMetricVSliderWidth, &scaleWidth); + aResult->SizeTo(scaleWidth, scaleWidth); + *aIsOverridable = false; + break; + } + + case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: + case NS_THEME_SCROLLBARTHUMB_VERTICAL: + { + // Find our parent scrollbar frame in order to find out whether we're in + // a small or a large scrollbar. + nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); + if (!scrollbarFrame) + return NS_ERROR_FAILURE; + + bool isSmall = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); + bool isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTHUMB_HORIZONTAL); + int32_t& minSize = isHorizontal ? aResult->width : aResult->height; + minSize = isSmall ? kSmallScrollbarThumbMinSize : kRegularScrollbarThumbMinSize; + break; + } + + case NS_THEME_SCROLLBAR: + case NS_THEME_SCROLLBAR_SMALL: + case NS_THEME_SCROLLBARTRACK_VERTICAL: + case NS_THEME_SCROLLBARTRACK_HORIZONTAL: + { + *aIsOverridable = false; + + if (nsLookAndFeel::UseOverlayScrollbars()) { + nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); + if (scrollbarFrame && + scrollbarFrame->StyleDisplay()->mAppearance == + NS_THEME_SCROLLBAR_SMALL) { + aResult->SizeTo(14, 14); + } + else { + aResult->SizeTo(16, 16); + } + break; + } + + // yeah, i know i'm cheating a little here, but i figure that it + // really doesn't matter if the scrollbar is vertical or horizontal + // and the width metric is a really good metric for every piece + // of the scrollbar. + + nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); + if (!scrollbarFrame) return NS_ERROR_FAILURE; + + int32_t themeMetric = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) ? + kThemeMetricSmallScrollBarWidth : + kThemeMetricScrollBarWidth; + SInt32 scrollbarWidth = 0; + ::GetThemeMetric(themeMetric, &scrollbarWidth); + aResult->SizeTo(scrollbarWidth, scrollbarWidth); + break; + } + + case NS_THEME_SCROLLBAR_NON_DISAPPEARING: + { + int32_t themeMetric = kThemeMetricScrollBarWidth; + + if (aFrame) { + nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); + if (scrollbarFrame && + scrollbarFrame->StyleDisplay()->mAppearance == + NS_THEME_SCROLLBAR_SMALL) { + // XXX We're interested in the width of non-disappearing scrollbars + // to leave enough space for a dropmarker in non-native styled + // comboboxes (bug 869314). It isn't clear to me if comboboxes can + // ever have small scrollbars. + themeMetric = kThemeMetricSmallScrollBarWidth; + } + } + + SInt32 scrollbarWidth = 0; + ::GetThemeMetric(themeMetric, &scrollbarWidth); + aResult->SizeTo(scrollbarWidth, scrollbarWidth); + break; + } + + case NS_THEME_SCROLLBARBUTTON_UP: + case NS_THEME_SCROLLBARBUTTON_DOWN: + case NS_THEME_SCROLLBARBUTTON_LEFT: + case NS_THEME_SCROLLBARBUTTON_RIGHT: + { + nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); + if (!scrollbarFrame) return NS_ERROR_FAILURE; + + // Since there is no NS_THEME_SCROLLBARBUTTON_UP_SMALL we need to ask the parent what appearance style it has. + int32_t themeMetric = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) ? + kThemeMetricSmallScrollBarWidth : + kThemeMetricScrollBarWidth; + SInt32 scrollbarWidth = 0; + ::GetThemeMetric(themeMetric, &scrollbarWidth); + + // It seems that for both sizes of scrollbar, the buttons are one pixel "longer". + if (aWidgetType == NS_THEME_SCROLLBARBUTTON_LEFT || aWidgetType == NS_THEME_SCROLLBARBUTTON_RIGHT) + aResult->SizeTo(scrollbarWidth+1, scrollbarWidth); + else + aResult->SizeTo(scrollbarWidth, scrollbarWidth+1); + + *aIsOverridable = false; + break; + } + case NS_THEME_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); + aResult->SizeTo(bounds.size.width, bounds.size.height); + *aIsOverridable = false; + } + } + + if (IsHiDPIContext(aPresContext)) { + *aResult = *aResult * 2; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame, uint8_t aWidgetType, + nsIAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) +{ + // Some widget types just never change state. + switch (aWidgetType) { + case NS_THEME_WINDOW_TITLEBAR: + case NS_THEME_TOOLBOX: + case NS_THEME_TOOLBAR: + case NS_THEME_STATUSBAR: + case NS_THEME_STATUSBARPANEL: + case NS_THEME_RESIZERPANEL: + case NS_THEME_TOOLTIP: + case NS_THEME_TABPANELS: + case NS_THEME_TABPANEL: + case NS_THEME_DIALOG: + case NS_THEME_MENUPOPUP: + case NS_THEME_GROUPBOX: + case NS_THEME_PROGRESSCHUNK: + case NS_THEME_PROGRESSCHUNK_VERTICAL: + case NS_THEME_PROGRESSBAR: + case NS_THEME_PROGRESSBAR_VERTICAL: + case NS_THEME_METERBAR: + case NS_THEME_METERCHUNK: + case NS_THEME_MAC_VIBRANCY_LIGHT: + case NS_THEME_MAC_VIBRANCY_DARK: + *aShouldRepaint = false; + return NS_OK; + } + + // 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 Mac OS X doesn't have themes. + return NS_OK; +} + +bool +nsNativeThemeCocoa::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, + uint8_t aWidgetType) +{ + // We don't have CSS set up to render non-native scrollbars on Mac OS X so we + // render natively even if native theme support is disabled. + if (aWidgetType != NS_THEME_SCROLLBAR && + aPresContext && !aPresContext->PresShell()->IsThemeSupportEnabled()) + return false; + + // if this is a dropdown button in a combobox the answer is always no + if (aWidgetType == NS_THEME_MENULIST_BUTTON) { + nsIFrame* parentFrame = aFrame->GetParent(); + if (parentFrame && (parentFrame->GetType() == nsGkAtoms::comboboxControlFrame)) + return false; + } + + switch (aWidgetType) { + // Combobox dropdowns don't support native theming in vertical mode. + case NS_THEME_MENULIST: + case NS_THEME_MENULIST_BUTTON: + case NS_THEME_MENULIST_TEXT: + case NS_THEME_MENULIST_TEXTFIELD: + if (aFrame && aFrame->GetWritingMode().IsVertical()) { + return false; + } + MOZ_FALLTHROUGH; + + case NS_THEME_LISTBOX: + + case NS_THEME_DIALOG: + case NS_THEME_WINDOW: + case NS_THEME_WINDOW_BUTTON_BOX: + case NS_THEME_WINDOW_TITLEBAR: + case NS_THEME_CHECKMENUITEM: + case NS_THEME_MENUPOPUP: + case NS_THEME_MENUARROW: + case NS_THEME_MENUITEM: + case NS_THEME_MENUSEPARATOR: + case NS_THEME_MAC_FULLSCREEN_BUTTON: + case NS_THEME_TOOLTIP: + + case NS_THEME_CHECKBOX: + case NS_THEME_CHECKBOX_CONTAINER: + case NS_THEME_RADIO: + case NS_THEME_RADIO_CONTAINER: + case NS_THEME_GROUPBOX: + case NS_THEME_MAC_HELP_BUTTON: + case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: + case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: + case NS_THEME_BUTTON: + case NS_THEME_BUTTON_ARROW_UP: + case NS_THEME_BUTTON_ARROW_DOWN: + case NS_THEME_BUTTON_BEVEL: + case NS_THEME_TOOLBARBUTTON: + case NS_THEME_SPINNER: + case NS_THEME_SPINNER_UPBUTTON: + case NS_THEME_SPINNER_DOWNBUTTON: + case NS_THEME_TOOLBAR: + case NS_THEME_STATUSBAR: + case NS_THEME_NUMBER_INPUT: + case NS_THEME_TEXTFIELD: + case NS_THEME_TEXTFIELD_MULTILINE: + case NS_THEME_SEARCHFIELD: + case NS_THEME_TOOLBOX: + //case NS_THEME_TOOLBARBUTTON: + case NS_THEME_PROGRESSBAR: + case NS_THEME_PROGRESSBAR_VERTICAL: + case NS_THEME_PROGRESSCHUNK: + case NS_THEME_PROGRESSCHUNK_VERTICAL: + case NS_THEME_METERBAR: + case NS_THEME_METERCHUNK: + case NS_THEME_SEPARATOR: + + case NS_THEME_TABPANELS: + case NS_THEME_TAB: + + case NS_THEME_TREETWISTY: + case NS_THEME_TREETWISTYOPEN: + case NS_THEME_TREEVIEW: + case NS_THEME_TREEHEADER: + case NS_THEME_TREEHEADERCELL: + case NS_THEME_TREEHEADERSORTARROW: + case NS_THEME_TREEITEM: + case NS_THEME_TREELINE: + case NS_THEME_MAC_SOURCE_LIST: + case NS_THEME_MAC_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: + + case NS_THEME_RANGE: + + case NS_THEME_SCALE_HORIZONTAL: + case NS_THEME_SCALETHUMB_HORIZONTAL: + case NS_THEME_SCALE_VERTICAL: + case NS_THEME_SCALETHUMB_VERTICAL: + + case NS_THEME_SCROLLBAR: + case NS_THEME_SCROLLBAR_SMALL: + case NS_THEME_SCROLLBARBUTTON_UP: + case NS_THEME_SCROLLBARBUTTON_DOWN: + case NS_THEME_SCROLLBARBUTTON_LEFT: + case NS_THEME_SCROLLBARBUTTON_RIGHT: + case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: + case NS_THEME_SCROLLBARTHUMB_VERTICAL: + case NS_THEME_SCROLLBARTRACK_VERTICAL: + case NS_THEME_SCROLLBARTRACK_HORIZONTAL: + case NS_THEME_SCROLLBAR_NON_DISAPPEARING: + return !IsWidgetStyled(aPresContext, aFrame, aWidgetType); + + case NS_THEME_RESIZER: + { + nsIFrame* parentFrame = aFrame->GetParent(); + if (!parentFrame || parentFrame->GetType() != nsGkAtoms::scrollFrame) + 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 (!nsLookAndFeel::UseOverlayScrollbars() && + scrollFrame && scrollFrame->GetScrollbarVisibility()); + } + + case NS_THEME_FOCUS_OUTLINE: + return true; + + case NS_THEME_MAC_VIBRANCY_LIGHT: + case NS_THEME_MAC_VIBRANCY_DARK: + return VibrancyManager::SystemSupportsVibrancy(); + } + + return false; +} + +bool +nsNativeThemeCocoa::WidgetIsContainer(uint8_t aWidgetType) +{ + // flesh this out at some point + switch (aWidgetType) { + case NS_THEME_MENULIST_BUTTON: + case NS_THEME_RADIO: + case NS_THEME_CHECKBOX: + case NS_THEME_PROGRESSBAR: + case NS_THEME_METERBAR: + case NS_THEME_RANGE: + case NS_THEME_MAC_HELP_BUTTON: + case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: + case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: + return false; + } + return true; +} + +bool +nsNativeThemeCocoa::ThemeDrawsFocusForWidget(uint8_t aWidgetType) +{ + if (aWidgetType == NS_THEME_MENULIST || + aWidgetType == NS_THEME_MENULIST_TEXTFIELD || + aWidgetType == NS_THEME_BUTTON || + aWidgetType == NS_THEME_MAC_HELP_BUTTON || + aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN || + aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED || + aWidgetType == NS_THEME_RADIO || + aWidgetType == NS_THEME_RANGE || + aWidgetType == NS_THEME_CHECKBOX) + return true; + + return false; +} + +bool +nsNativeThemeCocoa::ThemeNeedsComboboxDropmarker() +{ + return false; +} + +bool +nsNativeThemeCocoa::WidgetAppearanceDependsOnWindowFocus(uint8_t aWidgetType) +{ + switch (aWidgetType) { + case NS_THEME_DIALOG: + case NS_THEME_GROUPBOX: + case NS_THEME_TABPANELS: + case NS_THEME_BUTTON_ARROW_UP: + case NS_THEME_BUTTON_ARROW_DOWN: + case NS_THEME_CHECKMENUITEM: + case NS_THEME_MENUPOPUP: + case NS_THEME_MENUARROW: + case NS_THEME_MENUITEM: + case NS_THEME_MENUSEPARATOR: + case NS_THEME_TOOLTIP: + case NS_THEME_SPINNER: + case NS_THEME_SPINNER_UPBUTTON: + case NS_THEME_SPINNER_DOWNBUTTON: + case NS_THEME_SEPARATOR: + case NS_THEME_TOOLBOX: + case NS_THEME_NUMBER_INPUT: + case NS_THEME_TEXTFIELD: + case NS_THEME_TREEVIEW: + case NS_THEME_TREELINE: + case NS_THEME_TEXTFIELD_MULTILINE: + case NS_THEME_LISTBOX: + case NS_THEME_RESIZER: + return false; + default: + return true; + } +} + +bool +nsNativeThemeCocoa::IsWindowSheet(nsIFrame* aFrame) +{ + NSWindow* win = NativeWindowForFrame(aFrame); + id winDelegate = [win delegate]; + nsIWidget* widget = [(WindowDelegate *)winDelegate geckoWidget]; + if (!widget) { + return false; + } + return (widget->WindowType() == eWindowType_sheet); +} + +bool +nsNativeThemeCocoa::NeedToClearBackgroundBehindWidget(nsIFrame* aFrame, + uint8_t aWidgetType) +{ + switch (aWidgetType) { + case NS_THEME_MAC_SOURCE_LIST: + // If we're in a XUL tree, we don't want to clear the background behind the + // selections below, since that would make our source list to not pick up + // the right font smoothing background. But since we don't call this method + // in nsTreeBodyFrame::BuildDisplayList, we never get here. + case NS_THEME_MAC_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_VIBRANCY_LIGHT: + case NS_THEME_MAC_VIBRANCY_DARK: + case NS_THEME_TOOLTIP: + case NS_THEME_MENUPOPUP: + case NS_THEME_MENUITEM: + case NS_THEME_CHECKMENUITEM: + return true; + case NS_THEME_DIALOG: + return IsWindowSheet(aFrame); + default: + return false; + } +} + +static nscolor ConvertNSColor(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)); +} + +bool +nsNativeThemeCocoa::WidgetProvidesFontSmoothingBackgroundColor(nsIFrame* aFrame, + uint8_t aWidgetType, + nscolor* aColor) +{ + switch (aWidgetType) { + case NS_THEME_MAC_SOURCE_LIST: + case NS_THEME_MAC_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: + case NS_THEME_MAC_VIBRANCY_LIGHT: + case NS_THEME_MAC_VIBRANCY_DARK: + case NS_THEME_TOOLTIP: + case NS_THEME_MENUPOPUP: + case NS_THEME_MENUITEM: + case NS_THEME_CHECKMENUITEM: + case NS_THEME_DIALOG: + { + if ((aWidgetType == NS_THEME_DIALOG && !IsWindowSheet(aFrame)) || + ((aWidgetType == NS_THEME_MAC_SOURCE_LIST_SELECTION || + aWidgetType == NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION) && + !IsInSourceList(aFrame))) { + return false; + } + ChildView* childView = ChildViewForFrame(aFrame); + if (childView) { + ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); + NSColor* color = [childView vibrancyFontSmoothingBackgroundColorForThemeGeometryType:type]; + *aColor = ConvertNSColor(color); + return true; + } + return false; + } + default: + return false; + } +} + +nsITheme::ThemeGeometryType +nsNativeThemeCocoa::ThemeGeometryTypeForWidget(nsIFrame* aFrame, uint8_t aWidgetType) +{ + switch (aWidgetType) { + case NS_THEME_WINDOW_TITLEBAR: + return eThemeGeometryTypeTitlebar; + case NS_THEME_TOOLBAR: + return eThemeGeometryTypeToolbar; + case NS_THEME_TOOLBOX: + return eThemeGeometryTypeToolbox; + case NS_THEME_WINDOW_BUTTON_BOX: + return eThemeGeometryTypeWindowButtons; + case NS_THEME_MAC_FULLSCREEN_BUTTON: + return eThemeGeometryTypeFullscreenButton; + case NS_THEME_MAC_VIBRANCY_LIGHT: + return eThemeGeometryTypeVibrancyLight; + case NS_THEME_MAC_VIBRANCY_DARK: + return eThemeGeometryTypeVibrancyDark; + case NS_THEME_TOOLTIP: + return eThemeGeometryTypeTooltip; + case NS_THEME_MENUPOPUP: + return eThemeGeometryTypeMenu; + case NS_THEME_MENUITEM: + case NS_THEME_CHECKMENUITEM: { + EventStates eventState = GetContentState(aFrame, aWidgetType); + bool isDisabled = IsDisabled(aFrame, eventState); + bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + return isSelected ? eThemeGeometryTypeHighlightedMenuItem : eThemeGeometryTypeMenu; + } + case NS_THEME_DIALOG: + return IsWindowSheet(aFrame) ? eThemeGeometryTypeSheet : eThemeGeometryTypeUnknown; + case NS_THEME_MAC_SOURCE_LIST: + return eThemeGeometryTypeSourceList; + case NS_THEME_MAC_SOURCE_LIST_SELECTION: + return IsInSourceList(aFrame) ? eThemeGeometryTypeSourceListSelection + : eThemeGeometryTypeUnknown; + case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: + return IsInSourceList(aFrame) ? eThemeGeometryTypeActiveSourceListSelection + : eThemeGeometryTypeUnknown; + default: + return eThemeGeometryTypeUnknown; + } +} + +nsITheme::Transparency +nsNativeThemeCocoa::GetWidgetTransparency(nsIFrame* aFrame, uint8_t aWidgetType) +{ + switch (aWidgetType) { + case NS_THEME_MENUPOPUP: + case NS_THEME_TOOLTIP: + return eTransparent; + + case NS_THEME_DIALOG: + return IsWindowSheet(aFrame) ? eTransparent : eOpaque; + + case NS_THEME_SCROLLBAR_SMALL: + case NS_THEME_SCROLLBAR: + return nsLookAndFeel::UseOverlayScrollbars() ? eTransparent : eOpaque; + + case NS_THEME_STATUSBAR: + // Knowing that scrollbars and statusbars are opaque improves + // performance, because we create layers for them. + return eOpaque; + + case NS_THEME_TOOLBAR: + return eOpaque; + + default: + return eUnknownTransparency; + } +} diff --git a/widget/cocoa/nsNativeThemeColors.h b/widget/cocoa/nsNativeThemeColors.h new file mode 100644 index 000000000..b1691b516 --- /dev/null +++ b/widget/cocoa/nsNativeThemeColors.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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_ + +#include "nsCocoaFeatures.h" +#import <Cocoa/Cocoa.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 +}; + +__attribute__((unused)) +static int NativeGreyColorAsInt(ColorName name, BOOL isMain) +{ + if (nsCocoaFeatures::OnYosemiteOrLater()) + return sYosemiteThemeColors[name][isMain ? 0 : 1]; + return sLionThemeColors[name][isMain ? 0 : 1]; +} + +__attribute__((unused)) +static float NativeGreyColorAsFloat(ColorName name, BOOL isMain) +{ + return NativeGreyColorAsInt(name, isMain) / 255.0f; +} + +__attribute__((unused)) +static 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); +} + +#endif // nsNativeThemeColors_h_ diff --git a/widget/cocoa/nsPIWidgetCocoa.idl b/widget/cocoa/nsPIWidgetCocoa.idl new file mode 100644 index 000000000..a8fd8149c --- /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 000000000..470f17d99 --- /dev/null +++ b/widget/cocoa/nsPrintDialogX.h @@ -0,0 +1,68 @@ +/* -*- 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 : public nsIPrintDialogService +{ +public: + nsPrintDialogServiceX(); + + NS_DECL_ISUPPORTS + + NS_IMETHOD Init() override; + NS_IMETHOD Show(nsPIDOMWindowOuter *aParent, nsIPrintSettings *aSettings, + nsIWebBrowserPrint *aWebBrowserPrint) override; + NS_IMETHOD ShowPageSetup(nsPIDOMWindowOuter *aParent, + nsIPrintSettings *aSettings) override; + +protected: + virtual ~nsPrintDialogServiceX(); +}; + +@interface PrintPanelAccessoryView : NSView +{ + nsIPrintSettings* mSettings; + nsIStringBundle* mPrintBundle; + NSButton* mPrintSelectionOnlyCheckbox; + NSButton* mShrinkToFitCheckbox; + NSButton* mPrintBGColorsCheckbox; + NSButton* mPrintBGImagesCheckbox; + NSButtonCell* mAsLaidOutRadio; + NSButtonCell* mSelectedFrameRadio; + NSButtonCell* mSeparateFramesRadio; + NSPopUpButton* mHeaderLeftList; + NSPopUpButton* mHeaderCenterList; + NSPopUpButton* mHeaderRightList; + NSPopUpButton* mFooterLeftList; + NSPopUpButton* mFooterCenterList; + NSPopUpButton* mFooterRightList; +} + +- (id)initWithSettings:(nsIPrintSettings*)aSettings; + +- (void)exportSettings; + +@end + +@interface PrintPanelAccessoryController : NSViewController <NSPrintPanelAccessorizing> + +- (id)initWithSettings:(nsIPrintSettings*)aSettings; + +- (void)exportSettings; + +@end + +#endif // nsPrintDialog_h_ diff --git a/widget/cocoa/nsPrintDialogX.mm b/widget/cocoa/nsPrintDialogX.mm new file mode 100644 index 000000000..a6d58d5bf --- /dev/null +++ b/widget/cocoa/nsPrintDialogX.mm @@ -0,0 +1,682 @@ +/* -*- 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 "nsPrintDialogX.h" +#include "nsIPrintSettings.h" +#include "nsIPrintSettingsService.h" +#include "nsPrintSettingsX.h" +#include "nsCOMPtr.h" +#include "nsQueryObject.h" +#include "nsServiceManagerUtils.h" +#include "nsIWebProgressListener.h" +#include "nsIStringBundle.h" +#include "nsIWebBrowserPrint.h" +#include "nsCRT.h" + +#import <Cocoa/Cocoa.h> +#include "nsObjCExceptions.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsPrintDialogServiceX, nsIPrintDialogService) + +nsPrintDialogServiceX::nsPrintDialogServiceX() +{ +} + +nsPrintDialogServiceX::~nsPrintDialogServiceX() +{ +} + +NS_IMETHODIMP +nsPrintDialogServiceX::Init() +{ + return NS_OK; +} + +NS_IMETHODIMP +nsPrintDialogServiceX::Show(nsPIDOMWindowOuter *aParent, nsIPrintSettings *aSettings, + nsIWebBrowserPrint *aWebBrowserPrint) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NS_PRECONDITION(aSettings, "aSettings must not be null"); + + RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings)); + if (!settingsX) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIPrintSettingsService> printSettingsSvc + = do_GetService("@mozilla.org/gfx/printsettings-service;1"); + + // Set the print job title + char16_t** docTitles; + uint32_t titleCount; + nsresult rv = aWebBrowserPrint->EnumerateDocumentNames(&titleCount, &docTitles); + if (NS_SUCCEEDED(rv) && titleCount > 0) { + CFStringRef cfTitleString = CFStringCreateWithCharacters(NULL, reinterpret_cast<const UniChar*>(docTitles[0]), + NS_strlen(docTitles[0])); + if (cfTitleString) { + ::PMPrintSettingsSetJobName(settingsX->GetPMPrintSettings(), cfTitleString); + CFRelease(cfTitleString); + } + for (int32_t i = titleCount - 1; i >= 0; i--) { + free(docTitles[i]); + } + free(docTitles); + docTitles = NULL; + titleCount = 0; + } + + // Read default print settings from prefs + printSettingsSvc->InitPrintSettingsFromPrefs(settingsX, true, + nsIPrintSettings::kInitSaveNativeData); + NSPrintInfo* printInfo = settingsX->GetCocoaPrintInfo(); + + // 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]; + [panel addAccessoryController:viewController]; + [viewController release]; + + // Show the dialog. + nsCocoaUtils::PrepareForNativeAppModalDialog(); + int button = [panel runModal]; + nsCocoaUtils::CleanUpAfterNativeAppModalDialog(); + + NSPrintInfo* copy = [[[NSPrintOperation currentOperation] printInfo] copy]; + if (!copy) { + return NS_ERROR_OUT_OF_MEMORY; + } + + [NSPrintOperation setCurrentOperation:nil]; + [tmpView release]; + + if (button != NSFileHandlingPanelOKButton) + return NS_ERROR_ABORT; + + settingsX->SetCocoaPrintInfo(copy); + settingsX->InitUnwriteableMargin(); + + // Save settings unless saving is pref'd off + if (Preferences::GetBool("print.save_print_settings", false)) { + printSettingsSvc->SavePrintSettingsToPrefs(settingsX, true, + nsIPrintSettings::kInitSaveNativeData); + } + + // Get coordinate space resolution for converting paper size units to inches + NSWindow *win = [[NSApplication sharedApplication] mainWindow]; + if (win) { + NSDictionary *devDesc = [win deviceDescription]; + if (devDesc) { + NSSize res = [[devDesc objectForKey: NSDeviceResolution] sizeValue]; + float scale = [win backingScaleFactor]; + if (scale > 0) { + settingsX->SetInchesScale(res.width / scale, res.height / scale); + } + } + } + + // Export settings. + [viewController exportSettings]; + + // If "ignore scaling" is checked, overwrite scaling factor with 1. + bool isShrinkToFitChecked; + settingsX->GetShrinkToFit(&isShrinkToFitChecked); + if (isShrinkToFitChecked) { + NSMutableDictionary* dict = [copy dictionary]; + if (dict) { + [dict setObject: [NSNumber numberWithFloat: 1] + forKey: NSPrintScalingFactor]; + } + // Set the scaling factor to 100% in the NSPrintInfo + // object so that it will not affect the paper size + // retrieved from the PMPageFormat routines. + [copy setScalingFactor:1.0]; + } else { + aSettings->SetScaling([copy scalingFactor]); + } + + // Set the adjusted paper size now that we've updated + // the scaling factor. + settingsX->InitAdjustedPaperSize(); + + [copy release]; + + int16_t pageRange; + aSettings->GetPrintRange(&pageRange); + if (pageRange != nsIPrintSettings::kRangeSelection) { + PMPrintSettings nativePrintSettings = settingsX->GetPMPrintSettings(); + UInt32 firstPage, lastPage; + OSStatus status = ::PMGetFirstPage(nativePrintSettings, &firstPage); + if (status == noErr) { + status = ::PMGetLastPage(nativePrintSettings, &lastPage); + if (status == noErr && lastPage != UINT32_MAX) { + aSettings->SetPrintRange(nsIPrintSettings::kRangeSpecifiedPageRange); + aSettings->SetStartPageRange(firstPage); + aSettings->SetEndPageRange(lastPage); + } + } + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsPrintDialogServiceX::ShowPageSetup(nsPIDOMWindowOuter *aParent, + nsIPrintSettings *aNSSettings) +{ + NS_PRECONDITION(aParent, "aParent must not be null"); + NS_PRECONDITION(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->GetCocoaPrintInfo(); + NSPageLayout *pageLayout = [NSPageLayout pageLayout]; + nsCocoaUtils::PrepareForNativeAppModalDialog(); + int button = [pageLayout runModalWithPrintInfo:printInfo]; + nsCocoaUtils::CleanUpAfterNativeAppModalDialog(); + + return button == NSFileHandlingPanelOKButton ? NS_OK : NS_ERROR_ABORT; +} + +// Accessory view + +@interface PrintPanelAccessoryView (Private) + +- (NSString*)localizedString:(const char*)aKey; + +- (int16_t)chosenFrameSetting; + +- (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 char16_t*)aCurrentString; + +- (void)addOptionsSection; + +- (void)addAppearanceSection; + +- (void)addFramesSection; + +- (void)addHeaderFooterSection; + +- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox; + +- (NSString*)framesSummaryValue; + +- (NSString*)headerSummaryValue; + +- (NSString*)footerSummaryValue; + +@end + +static const char sHeaderFooterTags[][4] = {"", "&T", "&U", "&D", "&P", "&PT"}; + +@implementation PrintPanelAccessoryView + +// Public methods + +- (id)initWithSettings:(nsIPrintSettings*)aSettings +{ + [super initWithFrame:NSMakeRect(0, 0, 540, 270)]; + + mSettings = aSettings; + [self initBundle]; + [self addOptionsSection]; + [self addAppearanceSection]; + [self addFramesSection]; + [self addHeaderFooterSection]; + + return self; +} + +- (void)exportSettings +{ + mSettings->SetPrintRange([mPrintSelectionOnlyCheckbox state] == NSOnState ? + (int16_t)nsIPrintSettings::kRangeSelection : + (int16_t)nsIPrintSettings::kRangeAllPages); + mSettings->SetShrinkToFit([mShrinkToFitCheckbox state] == NSOnState); + mSettings->SetPrintBGColors([mPrintBGColorsCheckbox state] == NSOnState); + mSettings->SetPrintBGImages([mPrintBGImagesCheckbox state] == NSOnState); + mSettings->SetPrintFrameType([self chosenFrameSetting]); + + [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 @""; + + nsXPIDLString intlString; + mPrintBundle->GetStringFromName(NS_ConvertUTF8toUTF16(aKey).get(), getter_Copies(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:NSRightTextAlignment]; +} + +- (void)addCenteredLabel:(const char*)aLabel withFrame:(NSRect)aRect +{ + [self addLabel:aLabel withFrame:aRect alignment:NSCenterTextAlignment]; +} + +- (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 char16_t*)aCurrentString +{ + NSPopUpButton* list = [[[NSPopUpButton alloc] initWithFrame:aRect pullsDown:NO] autorelease]; + [list setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [[list cell] setControlSize:NSSmallControlSize]; + 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 +{ + // Title + [self addLabel:"optionsTitleMac" withFrame:NSMakeRect(0, 240, 151, 22)]; + + // "Print Selection Only" + mPrintSelectionOnlyCheckbox = [self checkboxWithLabel:"selectionOnly" + andFrame:NSMakeRect(156, 240, 0, 0)]; + + bool canPrintSelection; + mSettings->GetPrintOptions(nsIPrintSettings::kEnableSelectionRB, + &canPrintSelection); + [mPrintSelectionOnlyCheckbox setEnabled:canPrintSelection]; + + int16_t printRange; + mSettings->GetPrintRange(&printRange); + if (printRange == nsIPrintSettings::kRangeSelection) { + [mPrintSelectionOnlyCheckbox setState:NSOnState]; + } + + [self addSubview:mPrintSelectionOnlyCheckbox]; + + // "Shrink To Fit" + mShrinkToFitCheckbox = [self checkboxWithLabel:"shrinkToFit" + andFrame:NSMakeRect(156, 218, 0, 0)]; + + bool shrinkToFit; + mSettings->GetShrinkToFit(&shrinkToFit); + [mShrinkToFitCheckbox setState:(shrinkToFit ? NSOnState : NSOffState)]; + + [self addSubview:mShrinkToFitCheckbox]; +} + +- (void)addAppearanceSection +{ + // Title + [self addLabel:"appearanceTitleMac" withFrame:NSMakeRect(0, 188, 151, 22)]; + + // "Print Background Colors" + mPrintBGColorsCheckbox = [self checkboxWithLabel:"printBGColors" + andFrame:NSMakeRect(156, 188, 0, 0)]; + + bool geckoBool; + mSettings->GetPrintBGColors(&geckoBool); + [mPrintBGColorsCheckbox setState:(geckoBool ? NSOnState : NSOffState)]; + + [self addSubview:mPrintBGColorsCheckbox]; + + // "Print Background Images" + mPrintBGImagesCheckbox = [self checkboxWithLabel:"printBGImages" + andFrame:NSMakeRect(156, 166, 0, 0)]; + + mSettings->GetPrintBGImages(&geckoBool); + [mPrintBGImagesCheckbox setState:(geckoBool ? NSOnState : NSOffState)]; + + [self addSubview:mPrintBGImagesCheckbox]; +} + +- (void)addFramesSection +{ + // Title + [self addLabel:"framesTitleMac" withFrame:NSMakeRect(0, 124, 151, 22)]; + + // Radio matrix + NSButtonCell *radio = [[NSButtonCell alloc] init]; + [radio setButtonType:NSRadioButton]; + [radio setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]]; + NSMatrix *matrix = [[NSMatrix alloc] initWithFrame:NSMakeRect(156, 81, 400, 66) + mode:NSRadioModeMatrix + prototype:(NSCell*)radio + numberOfRows:3 + numberOfColumns:1]; + [radio release]; + [matrix setCellSize:NSMakeSize(400, 21)]; + [self addSubview:matrix]; + [matrix release]; + NSArray *cellArray = [matrix cells]; + mAsLaidOutRadio = [cellArray objectAtIndex:0]; + mSelectedFrameRadio = [cellArray objectAtIndex:1]; + mSeparateFramesRadio = [cellArray objectAtIndex:2]; + [mAsLaidOutRadio setTitle:[self localizedString:"asLaidOut"]]; + [mSelectedFrameRadio setTitle:[self localizedString:"selectedFrame"]]; + [mSeparateFramesRadio setTitle:[self localizedString:"separateFrames"]]; + + // Radio enabled state + int16_t frameUIFlag; + mSettings->GetHowToEnableFrameUI(&frameUIFlag); + if (frameUIFlag == nsIPrintSettings::kFrameEnableNone) { + [mAsLaidOutRadio setEnabled:NO]; + [mSelectedFrameRadio setEnabled:NO]; + [mSeparateFramesRadio setEnabled:NO]; + } else if (frameUIFlag == nsIPrintSettings::kFrameEnableAsIsAndEach) { + [mSelectedFrameRadio setEnabled:NO]; + } + + // Radio values + int16_t printFrameType; + mSettings->GetPrintFrameType(&printFrameType); + switch (printFrameType) { + case nsIPrintSettings::kFramesAsIs: + [mAsLaidOutRadio setState:NSOnState]; + break; + case nsIPrintSettings::kSelectedFrame: + [mSelectedFrameRadio setState:NSOnState]; + break; + case nsIPrintSettings::kEachFrameSep: + [mSeparateFramesRadio setState:NSOnState]; + break; + } +} + +- (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 + nsXPIDLString sel; + + mSettings->GetHeaderStrLeft(getter_Copies(sel)); + mHeaderLeftList = [self headerFooterItemListWithFrame:NSMakeRect(156, 44, 100, 22) + selectedItem:sel]; + [self addSubview:mHeaderLeftList]; + + mSettings->GetHeaderStrCenter(getter_Copies(sel)); + mHeaderCenterList = [self headerFooterItemListWithFrame:NSMakeRect(256, 44, 100, 22) + selectedItem:sel]; + [self addSubview:mHeaderCenterList]; + + mSettings->GetHeaderStrRight(getter_Copies(sel)); + mHeaderRightList = [self headerFooterItemListWithFrame:NSMakeRect(356, 44, 100, 22) + selectedItem:sel]; + [self addSubview:mHeaderRightList]; + + mSettings->GetFooterStrLeft(getter_Copies(sel)); + mFooterLeftList = [self headerFooterItemListWithFrame:NSMakeRect(156, 0, 100, 22) + selectedItem:sel]; + [self addSubview:mFooterLeftList]; + + mSettings->GetFooterStrCenter(getter_Copies(sel)); + mFooterCenterList = [self headerFooterItemListWithFrame:NSMakeRect(256, 0, 100, 22) + selectedItem:sel]; + [self addSubview:mFooterCenterList]; + + mSettings->GetFooterStrRight(getter_Copies(sel)); + mFooterRightList = [self headerFooterItemListWithFrame:NSMakeRect(356, 0, 100, 22) + selectedItem:sel]; + [self addSubview:mFooterRightList]; +} + +// Export settings + +- (int16_t)chosenFrameSetting +{ + if ([mAsLaidOutRadio state] == NSOnState) + return nsIPrintSettings::kFramesAsIs; + if ([mSelectedFrameRadio state] == NSOnState) + return nsIPrintSettings::kSelectedFrame; + if ([mSeparateFramesRadio state] == NSOnState) + return nsIPrintSettings::kEachFrameSep; + return nsIPrintSettings::kNoFrames; +} + +- (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).get()); + + headerFooterStr = [self headerFooterStringForList:mHeaderCenterList]; + mSettings->SetHeaderStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr).get()); + + headerFooterStr = [self headerFooterStringForList:mHeaderRightList]; + mSettings->SetHeaderStrRight(NS_ConvertUTF8toUTF16(headerFooterStr).get()); + + headerFooterStr = [self headerFooterStringForList:mFooterLeftList]; + mSettings->SetFooterStrLeft(NS_ConvertUTF8toUTF16(headerFooterStr).get()); + + headerFooterStr = [self headerFooterStringForList:mFooterCenterList]; + mSettings->SetFooterStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr).get()); + + headerFooterStr = [self headerFooterStringForList:mFooterRightList]; + mSettings->SetFooterStrRight(NS_ConvertUTF8toUTF16(headerFooterStr).get()); +} + +// Summary + +- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox +{ + if (![aCheckbox isEnabled]) + return [self localizedString:"summaryNAValue"]; + + return [aCheckbox state] == NSOnState ? + [self localizedString:"summaryOnValue"] : + [self localizedString:"summaryOffValue"]; +} + +- (NSString*)framesSummaryValue +{ + switch([self chosenFrameSetting]) { + case nsIPrintSettings::kFramesAsIs: + return [self localizedString:"asLaidOut"]; + case nsIPrintSettings::kSelectedFrame: + return [self localizedString:"selectedFrame"]; + case nsIPrintSettings::kEachFrameSep: + return [self localizedString:"separateFrames"]; + } + return [self localizedString:"summaryNAValue"]; +} + +- (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:"summaryFramesTitle"], NSPrintPanelAccessorySummaryItemNameKey, + [self framesSummaryValue], NSPrintPanelAccessorySummaryItemDescriptionKey, nil], + [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 +{ + [super initWithNibName:nil bundle:nil]; + + NSView* accView = [[PrintPanelAccessoryView alloc] initWithSettings:aSettings]; + [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/nsPrintOptionsX.h b/widget/cocoa/nsPrintOptionsX.h new file mode 100644 index 000000000..e34e75059 --- /dev/null +++ b/widget/cocoa/nsPrintOptionsX.h @@ -0,0 +1,44 @@ +/* -*- 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 nsPrintOptionsX_h_ +#define nsPrintOptionsX_h_ + +#include "nsPrintOptionsImpl.h" + +namespace mozilla +{ +namespace embedding +{ + class PrintData; +} // namespace embedding +} // namespace mozilla + +class nsPrintOptionsX : public nsPrintOptions +{ +public: + nsPrintOptionsX(); + virtual ~nsPrintOptionsX(); + + /* + * These serialize and deserialize methods are not symmetrical in that + * printSettingsX != deserialize(serialize(printSettingsX)). This is because + * the native print settings stored in the nsPrintSettingsX's NSPrintInfo + * object are not fully serialized. Only the values needed for successful + * printing are. + */ + NS_IMETHODIMP SerializeToPrintData(nsIPrintSettings* aSettings, + nsIWebBrowserPrint* aWBP, + mozilla::embedding::PrintData* data); + NS_IMETHODIMP DeserializeToPrintSettings(const mozilla::embedding::PrintData& data, + nsIPrintSettings* settings); + +protected: + nsresult _CreatePrintSettings(nsIPrintSettings **_retval); + nsresult ReadPrefs(nsIPrintSettings* aPS, const nsAString& aPrinterName, uint32_t aFlags); + nsresult WritePrefs(nsIPrintSettings* aPS, const nsAString& aPrinterName, uint32_t aFlags); +}; + +#endif // nsPrintOptionsX_h_ diff --git a/widget/cocoa/nsPrintOptionsX.mm b/widget/cocoa/nsPrintOptionsX.mm new file mode 100644 index 000000000..d9aa17b42 --- /dev/null +++ b/widget/cocoa/nsPrintOptionsX.mm @@ -0,0 +1,349 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsQueryObject.h" +#include "nsIServiceManager.h" +#include "nsPrintOptionsX.h" +#include "nsPrintSettingsX.h" + +// The constants for paper orientation were renamed in 10.9. __MAC_10_9 is +// defined on OS X 10.9 and later. Although 10.8 and earlier are not supported +// at this time, this allows for building on those older OS versions. The +// values are consistent across OS versions so the rename does not affect +// runtime, just compilation. +#ifdef __MAC_10_9 +#define NS_PAPER_ORIENTATION_PORTRAIT (NSPaperOrientationPortrait) +#define NS_PAPER_ORIENTATION_LANDSCAPE (NSPaperOrientationLandscape) +#else +#define NS_PAPER_ORIENTATION_PORTRAIT (NSPortraitOrientation) +#define NS_PAPER_ORIENTATION_LANDSCAPE (NSLandscapeOrientation) +#endif + +using namespace mozilla::embedding; + +nsPrintOptionsX::nsPrintOptionsX() +{ +} + +nsPrintOptionsX::~nsPrintOptionsX() +{ +} + +NS_IMETHODIMP +nsPrintOptionsX::SerializeToPrintData(nsIPrintSettings* aSettings, + nsIWebBrowserPrint* aWBP, + PrintData* data) +{ + nsresult rv = nsPrintOptions::SerializeToPrintData(aSettings, aWBP, data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aWBP) { + // When serializing an nsIWebBrowserPrint, we need to pass up the first + // document name. We could pass up the entire collection of document + // names, but the OS X printing prompt code only really cares about + // the first one, so we just send the first to save IPC traffic. + char16_t** docTitles; + uint32_t titleCount; + rv = aWBP->EnumerateDocumentNames(&titleCount, &docTitles); + if (NS_SUCCEEDED(rv) && titleCount > 0) { + data->printJobName().Assign(docTitles[0]); + } + + for (int32_t i = titleCount - 1; i >= 0; i--) { + free(docTitles[i]); + } + free(docTitles); + docTitles = nullptr; + } + + RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings)); + if (NS_WARN_IF(!settingsX)) { + return NS_ERROR_FAILURE; + } + + NSPrintInfo* printInfo = settingsX->GetCocoaPrintInfo(); + if (NS_WARN_IF(!printInfo)) { + return NS_ERROR_FAILURE; + } + + double adjustedWidth, adjustedHeight; + settingsX->GetAdjustedPaperSize(&adjustedWidth, &adjustedHeight); + data->adjustedPaperWidth() = adjustedWidth; + data->adjustedPaperHeight() = adjustedHeight; + + NSDictionary* dict = [printInfo dictionary]; + if (NS_WARN_IF(!dict)) { + return NS_ERROR_FAILURE; + } + + NSString* printerName = [dict objectForKey: NSPrintPrinterName]; + if (printerName) { + nsCocoaUtils::GetStringForNSString(printerName, data->printerName()); + } + + NSString* faxNumber = [dict objectForKey: NSPrintFaxNumber]; + if (faxNumber) { + nsCocoaUtils::GetStringForNSString(faxNumber, data->faxNumber()); + } + + NSURL* printToFileURL = [dict objectForKey: NSPrintJobSavingURL]; + if (printToFileURL) { + nsCocoaUtils::GetStringForNSString([printToFileURL absoluteString], + data->toFileName()); + } + + NSDate* printTime = [dict objectForKey: NSPrintTime]; + if (printTime) { + NSTimeInterval timestamp = [printTime timeIntervalSinceReferenceDate]; + data->printTime() = timestamp; + } + + NSString* disposition = [dict objectForKey: NSPrintJobDisposition]; + if (disposition) { + nsCocoaUtils::GetStringForNSString(disposition, data->disposition()); + } + + NSString* paperName = [dict objectForKey: NSPrintPaperName]; + if (paperName) { + nsCocoaUtils::GetStringForNSString(paperName, data->paperName()); + } + + float scalingFactor = [[dict objectForKey: NSPrintScalingFactor] floatValue]; + data->scalingFactor() = scalingFactor; + + int32_t orientation; + if ([printInfo orientation] == NS_PAPER_ORIENTATION_PORTRAIT) { + orientation = nsIPrintSettings::kPortraitOrientation; + } else { + orientation = nsIPrintSettings::kLandscapeOrientation; + } + data->orientation() = orientation; + + NSSize paperSize = [printInfo paperSize]; + float widthScale, heightScale; + settingsX->GetInchesScale(&widthScale, &heightScale); + if (orientation == nsIPrintSettings::kLandscapeOrientation) { + // switch widths and heights + data->widthScale() = heightScale; + data->heightScale() = widthScale; + data->paperWidth() = paperSize.height / heightScale; + data->paperHeight() = paperSize.width / widthScale; + } else { + data->widthScale() = widthScale; + data->heightScale() = heightScale; + data->paperWidth() = paperSize.width / widthScale; + data->paperHeight() = paperSize.height / heightScale; + } + + data->numCopies() = [[dict objectForKey: NSPrintCopies] intValue]; + data->printAllPages() = [[dict objectForKey: NSPrintAllPages] boolValue]; + data->startPageRange() = [[dict objectForKey: NSPrintFirstPage] intValue]; + data->endPageRange() = [[dict objectForKey: NSPrintLastPage] intValue]; + data->mustCollate() = [[dict objectForKey: NSPrintMustCollate] boolValue]; + data->printReversed() = [[dict objectForKey: NSPrintReversePageOrder] boolValue]; + data->pagesAcross() = [[dict objectForKey: NSPrintPagesAcross] unsignedShortValue]; + data->pagesDown() = [[dict objectForKey: NSPrintPagesDown] unsignedShortValue]; + data->detailedErrorReporting() = [[dict objectForKey: NSPrintDetailedErrorReporting] boolValue]; + data->addHeaderAndFooter() = [[dict objectForKey: NSPrintHeaderAndFooter] boolValue]; + data->fileNameExtensionHidden() = + [[dict objectForKey: NSPrintJobSavingFileNameExtensionHidden] boolValue]; + + bool printSelectionOnly = [[dict objectForKey: NSPrintSelectionOnly] boolValue]; + aSettings->SetPrintOptions(nsIPrintSettings::kEnableSelectionRB, + printSelectionOnly); + aSettings->GetPrintOptionsBits(&data->optionFlags()); + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintOptionsX::DeserializeToPrintSettings(const PrintData& data, + nsIPrintSettings* settings) +{ + nsresult rv = nsPrintOptions::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; + } + + NSPrintInfo* sharedPrintInfo = [NSPrintInfo sharedPrintInfo]; + if (NS_WARN_IF(!sharedPrintInfo)) { + return NS_ERROR_FAILURE; + } + + NSDictionary* sharedDict = [sharedPrintInfo dictionary]; + if (NS_WARN_IF(!sharedDict)) { + return NS_ERROR_FAILURE; + } + + // We need to create a new NSMutableDictionary to pass to NSPrintInfo with + // the values that we got from the other process. + NSMutableDictionary* newPrintInfoDict = + [NSMutableDictionary dictionaryWithDictionary:sharedDict]; + if (NS_WARN_IF(!newPrintInfoDict)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NSString* printerName = nsCocoaUtils::ToNSString(data.printerName()); + if (printerName) { + NSPrinter* printer = [NSPrinter printerWithName: printerName]; + if (printer) { + [newPrintInfoDict setObject: printer forKey: NSPrintPrinter]; + [newPrintInfoDict setObject: printerName forKey: NSPrintPrinterName]; + } + } + + [newPrintInfoDict setObject: [NSNumber numberWithInt: data.numCopies()] + forKey: NSPrintCopies]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.printAllPages()] + forKey: NSPrintAllPages]; + [newPrintInfoDict setObject: [NSNumber numberWithInt: data.startPageRange()] + forKey: NSPrintFirstPage]; + [newPrintInfoDict setObject: [NSNumber numberWithInt: data.endPageRange()] + forKey: NSPrintLastPage]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.mustCollate()] + forKey: NSPrintMustCollate]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.printReversed()] + forKey: NSPrintReversePageOrder]; + + [newPrintInfoDict setObject: nsCocoaUtils::ToNSString(data.disposition()) + forKey: NSPrintJobDisposition]; + + [newPrintInfoDict setObject: nsCocoaUtils::ToNSString(data.paperName()) + forKey: NSPrintPaperName]; + + [newPrintInfoDict setObject: [NSNumber numberWithFloat: data.scalingFactor()] + forKey: NSPrintScalingFactor]; + + CGFloat width = data.paperWidth() * data.widthScale(); + CGFloat height = data.paperHeight() * data.heightScale(); + [newPrintInfoDict setObject: [NSValue valueWithSize:NSMakeSize(width,height)] + forKey: NSPrintPaperSize]; + + int paperOrientation; + if (data.orientation() == nsIPrintSettings::kPortraitOrientation) { + paperOrientation = NS_PAPER_ORIENTATION_PORTRAIT; + settings->SetOrientation(nsIPrintSettings::kPortraitOrientation); + } else { + paperOrientation = NS_PAPER_ORIENTATION_LANDSCAPE; + settings->SetOrientation(nsIPrintSettings::kLandscapeOrientation); + } + [newPrintInfoDict setObject: [NSNumber numberWithInt:paperOrientation] + forKey: NSPrintOrientation]; + + [newPrintInfoDict setObject: [NSNumber numberWithShort: data.pagesAcross()] + forKey: NSPrintPagesAcross]; + [newPrintInfoDict setObject: [NSNumber numberWithShort: data.pagesDown()] + forKey: NSPrintPagesDown]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.detailedErrorReporting()] + forKey: NSPrintDetailedErrorReporting]; + [newPrintInfoDict setObject: nsCocoaUtils::ToNSString(data.faxNumber()) + forKey: NSPrintFaxNumber]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.addHeaderAndFooter()] + forKey: NSPrintHeaderAndFooter]; + [newPrintInfoDict setObject: [NSNumber numberWithBool: data.fileNameExtensionHidden()] + forKey: NSPrintJobSavingFileNameExtensionHidden]; + + // At this point, the base class should have properly deserialized the print + // options bitfield for nsIPrintSettings, so that it holds the correct value + // for kEnableSelectionRB, which we use to set NSPrintSelectionOnly. + + bool printSelectionOnly = false; + rv = settings->GetPrintOptions(nsIPrintSettings::kEnableSelectionRB, &printSelectionOnly); + if (NS_SUCCEEDED(rv)) { + [newPrintInfoDict setObject: [NSNumber numberWithBool: printSelectionOnly] + forKey: NSPrintSelectionOnly]; + } else { + [newPrintInfoDict setObject: [NSNumber numberWithBool: NO] + forKey: NSPrintSelectionOnly]; + } + + NSURL* jobSavingURL = + [NSURL URLWithString: nsCocoaUtils::ToNSString(data.toFileName())]; + if (jobSavingURL) { + [newPrintInfoDict setObject: jobSavingURL forKey: NSPrintJobSavingURL]; + } + + NSTimeInterval timestamp = data.printTime(); + NSDate* printTime = [NSDate dateWithTimeIntervalSinceReferenceDate: timestamp]; + if (printTime) { + [newPrintInfoDict setObject: printTime forKey: NSPrintTime]; + } + + // Next, we create a new NSPrintInfo with the values in our dictionary. + NSPrintInfo* newPrintInfo = + [[NSPrintInfo alloc] initWithDictionary: newPrintInfoDict]; + if (NS_WARN_IF(!newPrintInfo)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // And now swap in the new NSPrintInfo we've just populated. + settingsX->SetCocoaPrintInfo(newPrintInfo); + [newPrintInfo release]; + + settingsX->SetAdjustedPaperSize(data.adjustedPaperWidth(), + data.adjustedPaperHeight()); + + return NS_OK; +} + +nsresult +nsPrintOptionsX::ReadPrefs(nsIPrintSettings* aPS, const nsAString& aPrinterName, uint32_t aFlags) +{ + nsresult rv; + + rv = nsPrintOptions::ReadPrefs(aPS, aPrinterName, aFlags); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsPrintOptions::ReadPrefs() failed"); + + RefPtr<nsPrintSettingsX> printSettingsX(do_QueryObject(aPS)); + if (!printSettingsX) + return NS_ERROR_NO_INTERFACE; + rv = printSettingsX->ReadPageFormatFromPrefs(); + + return NS_OK; +} + +nsresult nsPrintOptionsX::_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; + } + + InitPrintSettingsFromPrefs(*_retval, false, nsIPrintSettings::kInitSaveAll); + return rv; +} + +nsresult +nsPrintOptionsX::WritePrefs(nsIPrintSettings* aPS, const nsAString& aPrinterName, uint32_t aFlags) +{ + nsresult rv; + + rv = nsPrintOptions::WritePrefs(aPS, aPrinterName, aFlags); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsPrintOptions::WritePrefs() failed"); + + RefPtr<nsPrintSettingsX> printSettingsX(do_QueryObject(aPS)); + if (!printSettingsX) + return NS_ERROR_NO_INTERFACE; + rv = printSettingsX->WritePageFormatToPrefs(); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsPrintSettingsX::WritePageFormatToPrefs() failed"); + + return NS_OK; +} diff --git a/widget/cocoa/nsPrintSettingsX.h b/widget/cocoa/nsPrintSettingsX.h new file mode 100644 index 000000000..1d755b250 --- /dev/null +++ b/widget/cocoa/nsPrintSettingsX.h @@ -0,0 +1,82 @@ +/* -*- 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> + +#define NS_PRINTSETTINGSX_IID \ +{ 0x0DF2FDBD, 0x906D, 0x4726, \ + { 0x9E, 0x4D, 0xCF, 0xE0, 0x87, 0x8D, 0x70, 0x7C } } + +class nsPrintSettingsX : public nsPrintSettings +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_PRINTSETTINGSX_IID) + NS_DECL_ISUPPORTS_INHERITED + + nsPrintSettingsX(); + nsresult Init(); + NSPrintInfo* GetCocoaPrintInfo() { return mPrintInfo; } + void SetCocoaPrintInfo(NSPrintInfo* aPrintInfo); + virtual nsresult ReadPageFormatFromPrefs(); + virtual nsresult WritePageFormatToPrefs(); + virtual nsresult GetEffectivePageSize(double *aWidth, + double *aHeight) override; + + // In addition to setting the paper width and height, these + // overrides set the adjusted width and height returned from + // GetEffectivePageSize. This is needed when a paper size is + // set manually without using a print dialog a la reftest-print. + virtual nsresult SetPaperWidth(double aPaperWidth) override; + virtual nsresult SetPaperHeight(double aPaperWidth) override; + + PMPrintSettings GetPMPrintSettings(); + PMPrintSession GetPMPrintSession(); + PMPageFormat GetPMPageFormat(); + void SetPMPageFormat(PMPageFormat aPageFormat); + + // Re-initialize mUnwriteableMargin with values from mPageFormat. + // Should be called whenever mPageFormat is initialized or overwritten. + nsresult InitUnwriteableMargin(); + + // Re-initialize mAdjustedPaper{Width,Height} with values from mPageFormat. + // Should be called whenever mPageFormat is initialized or overwritten. + nsresult InitAdjustedPaperSize(); + + void SetInchesScale(float aWidthScale, float aHeightScale); + void GetInchesScale(float *aWidthScale, float *aHeightScale); + + void SetAdjustedPaperSize(double aWidth, double aHeight); + void GetAdjustedPaperSize(double *aWidth, double *aHeight); + +protected: + virtual ~nsPrintSettingsX(); + + nsPrintSettingsX(const nsPrintSettingsX& src); + nsPrintSettingsX& operator=(const nsPrintSettingsX& rhs); + + nsresult _Clone(nsIPrintSettings **_retval) override; + nsresult _Assign(nsIPrintSettings *aPS) override; + + // The out param has a ref count of 1 on return so caller needs to PMRelase() when done. + OSStatus CreateDefaultPageFormat(PMPrintSession aSession, PMPageFormat& outFormat); + OSStatus CreateDefaultPrintSettings(PMPrintSession aSession, PMPrintSettings& outSettings); + + NSPrintInfo* mPrintInfo; + + // Scaling factors used to convert the NSPrintInfo + // paper size units to inches + float mWidthScale; + float mHeightScale; + double mAdjustedPaperWidth; + double mAdjustedPaperHeight; +}; + +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 000000000..73a8e78d2 --- /dev/null +++ b/widget/cocoa/nsPrintSettingsX.mm @@ -0,0 +1,272 @@ +/* -*- 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 "mozilla/Preferences.h" + +using namespace mozilla; + +#define MAC_OS_X_PAGE_SETUP_PREFNAME "print.macosx.pagesetup-2" +#define COCOA_PAPER_UNITS_PER_INCH 72.0 + +NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsX, nsPrintSettings, nsPrintSettingsX) + +nsPrintSettingsX::nsPrintSettingsX() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mPrintInfo = [[NSPrintInfo sharedPrintInfo] copy]; + mWidthScale = COCOA_PAPER_UNITS_PER_INCH; + mHeightScale = COCOA_PAPER_UNITS_PER_INCH; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsPrintSettingsX::nsPrintSettingsX(const nsPrintSettingsX& src) +{ + *this = src; +} + +nsPrintSettingsX::~nsPrintSettingsX() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mPrintInfo release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsPrintSettingsX& nsPrintSettingsX::operator=(const nsPrintSettingsX& rhs) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (this == &rhs) { + return *this; + } + + nsPrintSettings::operator=(rhs); + + [mPrintInfo release]; + mPrintInfo = [rhs.mPrintInfo copy]; + + return *this; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(*this); +} + +nsresult nsPrintSettingsX::Init() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + InitUnwriteableMargin(); + InitAdjustedPaperSize(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Should be called whenever the page format changes. +NS_IMETHODIMP nsPrintSettingsX::InitUnwriteableMargin() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + PMPaper paper; + PMPaperMargins paperMargin; + PMPageFormat pageFormat = GetPMPageFormat(); + ::PMGetPageFormatPaper(pageFormat, &paper); + ::PMPaperGetMargins(paper, &paperMargin); + mUnwriteableMargin.top = NS_POINTS_TO_INT_TWIPS(paperMargin.top); + mUnwriteableMargin.left = NS_POINTS_TO_INT_TWIPS(paperMargin.left); + mUnwriteableMargin.bottom = NS_POINTS_TO_INT_TWIPS(paperMargin.bottom); + mUnwriteableMargin.right = NS_POINTS_TO_INT_TWIPS(paperMargin.right); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsPrintSettingsX::InitAdjustedPaperSize() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + PMPageFormat pageFormat = GetPMPageFormat(); + + PMRect paperRect; + ::PMGetAdjustedPaperRect(pageFormat, &paperRect); + + mAdjustedPaperWidth = paperRect.right - paperRect.left; + mAdjustedPaperHeight = paperRect.bottom - paperRect.top; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void +nsPrintSettingsX::SetCocoaPrintInfo(NSPrintInfo* aPrintInfo) +{ + if (mPrintInfo != aPrintInfo) { + [mPrintInfo release]; + mPrintInfo = [aPrintInfo retain]; + } +} + +NS_IMETHODIMP nsPrintSettingsX::ReadPageFormatFromPrefs() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsAutoCString encodedData; + nsresult rv = + Preferences::GetCString(MAC_OS_X_PAGE_SETUP_PREFNAME, &encodedData); + if (NS_FAILED(rv)) { + return rv; + } + + // decode the base64 + char* decodedData = PL_Base64Decode(encodedData.get(), encodedData.Length(), nullptr); + NSData* data = [NSData dataWithBytes:decodedData length:strlen(decodedData)]; + if (!data) + return NS_ERROR_FAILURE; + + PMPageFormat newPageFormat; + OSStatus status = ::PMPageFormatCreateWithDataRepresentation((CFDataRef)data, &newPageFormat); + if (status == noErr) { + SetPMPageFormat(newPageFormat); + } + InitUnwriteableMargin(); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsPrintSettingsX::WritePageFormatToPrefs() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + PMPageFormat pageFormat = GetPMPageFormat(); + if (pageFormat == kPMNoPageFormat) + return NS_ERROR_NOT_INITIALIZED; + + NSData* data = nil; + OSStatus err = ::PMPageFormatCreateDataRepresentation(pageFormat, (CFDataRef*)&data, kPMDataFormatXMLDefault); + if (err != noErr) + return NS_ERROR_FAILURE; + + nsAutoCString encodedData; + encodedData.Adopt(PL_Base64Encode((char*)[data bytes], [data length], nullptr)); + if (!encodedData.get()) + return NS_ERROR_OUT_OF_MEMORY; + + return Preferences::SetCString(MAC_OS_X_PAGE_SETUP_PREFNAME, encodedData); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsPrintSettingsX::_Clone(nsIPrintSettings **_retval) +{ + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsPrintSettingsX *newSettings = new nsPrintSettingsX(*this); + if (!newSettings) + return NS_ERROR_FAILURE; + *_retval = newSettings; + NS_ADDREF(*_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; +} + +PMPrintSettings +nsPrintSettingsX::GetPMPrintSettings() +{ + return static_cast<PMPrintSettings>([mPrintInfo PMPrintSettings]); +} + +PMPrintSession +nsPrintSettingsX::GetPMPrintSession() +{ + return static_cast<PMPrintSession>([mPrintInfo PMPrintSession]); +} + +PMPageFormat +nsPrintSettingsX::GetPMPageFormat() +{ + return static_cast<PMPageFormat>([mPrintInfo PMPageFormat]); +} + +void +nsPrintSettingsX::SetPMPageFormat(PMPageFormat aPageFormat) +{ + PMPageFormat oldPageFormat = GetPMPageFormat(); + ::PMCopyPageFormat(aPageFormat, oldPageFormat); + [mPrintInfo updateFromPMPageFormat]; +} + +void +nsPrintSettingsX::SetInchesScale(float aWidthScale, float aHeightScale) +{ + if (aWidthScale > 0 && aHeightScale > 0) { + mWidthScale = aWidthScale; + mHeightScale = aHeightScale; + } +} + +void +nsPrintSettingsX::GetInchesScale(float *aWidthScale, float *aHeightScale) +{ + *aWidthScale = mWidthScale; + *aHeightScale = mHeightScale; +} + +NS_IMETHODIMP nsPrintSettingsX::SetPaperWidth(double aPaperWidth) +{ + mPaperWidth = aPaperWidth; + mAdjustedPaperWidth = aPaperWidth * mWidthScale; + return NS_OK; +} + +NS_IMETHODIMP nsPrintSettingsX::SetPaperHeight(double aPaperHeight) +{ + mPaperHeight = aPaperHeight; + mAdjustedPaperHeight = aPaperHeight * mHeightScale; + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsX::GetEffectivePageSize(double *aWidth, double *aHeight) +{ + *aWidth = NS_INCHES_TO_TWIPS(mAdjustedPaperWidth / mWidthScale); + *aHeight = NS_INCHES_TO_TWIPS(mAdjustedPaperHeight / mHeightScale); + return NS_OK; +} + +void nsPrintSettingsX::SetAdjustedPaperSize(double aWidth, double aHeight) +{ + mAdjustedPaperWidth = aWidth; + mAdjustedPaperHeight = aHeight; +} + +void nsPrintSettingsX::GetAdjustedPaperSize(double *aWidth, double *aHeight) +{ + *aWidth = mAdjustedPaperWidth; + *aHeight = mAdjustedPaperHeight; +} diff --git a/widget/cocoa/nsSandboxViolationSink.h b/widget/cocoa/nsSandboxViolationSink.h new file mode 100644 index 000000000..35b5d89af --- /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 000000000..057217334 --- /dev/null +++ b/widget/cocoa/nsSandboxViolationSink.mm @@ -0,0 +1,115 @@ +/* -*- 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" + +#include <unistd.h> +#include <time.h> +#include <asl.h> +#include <dispatch/dispatch.h> +#include <notify.h> +#include "nsCocoaDebugUtils.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); + nsCocoaDebugUtils::DebugLog("nsSandboxViolationSink::ViolationHandler(): id %s, pid %s, message %s", + id_str, pid_str, message_str); + } + aslresponse_free(response); + } +} diff --git a/widget/cocoa/nsScreenCocoa.h b/widget/cocoa/nsScreenCocoa.h new file mode 100644 index 000000000..268d5beb0 --- /dev/null +++ b/widget/cocoa/nsScreenCocoa.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 nsScreenCocoa_h_ +#define nsScreenCocoa_h_ + +#import <Cocoa/Cocoa.h> + +#include "nsBaseScreen.h" + +class nsScreenCocoa : public nsBaseScreen +{ +public: + explicit nsScreenCocoa (NSScreen *screen); + ~nsScreenCocoa (); + + NS_IMETHOD GetId(uint32_t* outId); + NS_IMETHOD GetRect(int32_t* aLeft, int32_t* aTop, int32_t* aWidth, int32_t* aHeight); + NS_IMETHOD GetAvailRect(int32_t* aLeft, int32_t* aTop, int32_t* aWidth, int32_t* aHeight); + NS_IMETHOD GetRectDisplayPix(int32_t* aLeft, int32_t* aTop, int32_t* aWidth, int32_t* aHeight); + NS_IMETHOD GetAvailRectDisplayPix(int32_t* aLeft, int32_t* aTop, int32_t* aWidth, int32_t* aHeight); + NS_IMETHOD GetPixelDepth(int32_t* aPixelDepth); + NS_IMETHOD GetColorDepth(int32_t* aColorDepth); + NS_IMETHOD GetContentsScaleFactor(double* aContentsScaleFactor); + NS_IMETHOD GetDefaultCSSScaleFactor(double* aScaleFactor) + { + return GetContentsScaleFactor(aScaleFactor); + } + + NSScreen *CocoaScreen() { return mScreen; } + +private: + CGFloat BackingScaleFactor(); + + NSScreen *mScreen; + uint32_t mId; +}; + +#endif // nsScreenCocoa_h_ diff --git a/widget/cocoa/nsScreenCocoa.mm b/widget/cocoa/nsScreenCocoa.mm new file mode 100644 index 000000000..08905bf0a --- /dev/null +++ b/widget/cocoa/nsScreenCocoa.mm @@ -0,0 +1,147 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsScreenCocoa.h" +#include "nsObjCExceptions.h" +#include "nsCocoaUtils.h" + +static uint32_t sScreenId = 0; + +nsScreenCocoa::nsScreenCocoa (NSScreen *screen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mScreen = [screen retain]; + mId = ++sScreenId; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsScreenCocoa::~nsScreenCocoa () +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mScreen release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetId(uint32_t *outId) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + *outId = mId; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetRect(int32_t *outX, int32_t *outY, int32_t *outWidth, int32_t *outHeight) +{ + NSRect frame = [mScreen frame]; + + mozilla::LayoutDeviceIntRect r = + nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor()); + + *outX = r.x; + *outY = r.y; + *outWidth = r.width; + *outHeight = r.height; + + return NS_OK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetAvailRect(int32_t *outX, int32_t *outY, int32_t *outWidth, int32_t *outHeight) +{ + NSRect frame = [mScreen visibleFrame]; + + mozilla::LayoutDeviceIntRect r = + nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor()); + + *outX = r.x; + *outY = r.y; + *outWidth = r.width; + *outHeight = r.height; + + return NS_OK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetRectDisplayPix(int32_t *outX, int32_t *outY, int32_t *outWidth, int32_t *outHeight) +{ + NSRect frame = [mScreen frame]; + + mozilla::DesktopIntRect r = nsCocoaUtils::CocoaRectToGeckoRect(frame); + + *outX = r.x; + *outY = r.y; + *outWidth = r.width; + *outHeight = r.height; + + return NS_OK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetAvailRectDisplayPix(int32_t *outX, int32_t *outY, int32_t *outWidth, int32_t *outHeight) +{ + NSRect frame = [mScreen visibleFrame]; + + mozilla::DesktopIntRect r = nsCocoaUtils::CocoaRectToGeckoRect(frame); + + *outX = r.x; + *outY = r.y; + *outWidth = r.width; + *outHeight = r.height; + + return NS_OK; +} + +NS_IMETHODIMP +nsScreenCocoa::GetPixelDepth(int32_t *aPixelDepth) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSWindowDepth depth = [mScreen depth]; + int bpp = NSBitsPerPixelFromDepth(depth); + + *aPixelDepth = bpp; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenCocoa::GetColorDepth(int32_t *aColorDepth) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSWindowDepth depth = [mScreen depth]; + int bpp = NSBitsPerPixelFromDepth (depth); + + *aColorDepth = bpp; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenCocoa::GetContentsScaleFactor(double *aContentsScaleFactor) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + *aContentsScaleFactor = (double) BackingScaleFactor(); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +CGFloat +nsScreenCocoa::BackingScaleFactor() +{ + return nsCocoaUtils::GetBackingScaleFactor(mScreen); +} diff --git a/widget/cocoa/nsScreenManagerCocoa.h b/widget/cocoa/nsScreenManagerCocoa.h new file mode 100644 index 000000000..61a059d97 --- /dev/null +++ b/widget/cocoa/nsScreenManagerCocoa.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 nsScreenManagerCocoa_h_ +#define nsScreenManagerCocoa_h_ + +#import <Cocoa/Cocoa.h> + +#include "mozilla/RefPtr.h" +#include "nsTArray.h" +#include "nsIScreenManager.h" +#include "nsScreenCocoa.h" + +class nsScreenManagerCocoa : public nsIScreenManager +{ +public: + nsScreenManagerCocoa(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISCREENMANAGER + +protected: + virtual ~nsScreenManagerCocoa(); + +private: + + nsScreenCocoa *ScreenForCocoaScreen(NSScreen *screen); + nsTArray< RefPtr<nsScreenCocoa> > mScreenList; +}; + +#endif // nsScreenManagerCocoa_h_ diff --git a/widget/cocoa/nsScreenManagerCocoa.mm b/widget/cocoa/nsScreenManagerCocoa.mm new file mode 100644 index 000000000..9a0cbb9cc --- /dev/null +++ b/widget/cocoa/nsScreenManagerCocoa.mm @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsScreenManagerCocoa.h" +#include "nsObjCExceptions.h" +#include "nsCOMPtr.h" +#include "nsCocoaUtils.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsScreenManagerCocoa, nsIScreenManager) + +nsScreenManagerCocoa::nsScreenManagerCocoa() +{ +} + +nsScreenManagerCocoa::~nsScreenManagerCocoa() +{ +} + +nsScreenCocoa* +nsScreenManagerCocoa::ScreenForCocoaScreen(NSScreen *screen) +{ + for (uint32_t i = 0; i < mScreenList.Length(); ++i) { + nsScreenCocoa* sc = mScreenList[i]; + if (sc->CocoaScreen() == screen) { + // doesn't addref + return sc; + } + } + + // didn't find it; create and insert + RefPtr<nsScreenCocoa> sc = new nsScreenCocoa(screen); + mScreenList.AppendElement(sc); + return sc.get(); +} + +NS_IMETHODIMP +nsScreenManagerCocoa::ScreenForId (uint32_t aId, nsIScreen **outScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT + + *outScreen = nullptr; + + for (uint32_t i = 0; i < mScreenList.Length(); ++i) { + nsScreenCocoa* sc = mScreenList[i]; + uint32_t id; + nsresult rv = sc->GetId(&id); + + if (NS_SUCCEEDED(rv) && id == aId) { + *outScreen = sc; + NS_ADDREF(*outScreen); + return NS_OK; + } + } + + return NS_ERROR_FAILURE; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenManagerCocoa::ScreenForRect (int32_t aX, int32_t aY, + int32_t aWidth, int32_t aHeight, + nsIScreen **outScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSEnumerator *screenEnum = [[NSScreen screens] objectEnumerator]; + NSRect inRect = + nsCocoaUtils::GeckoRectToCocoaRect(DesktopIntRect(aX, aY, + aWidth, aHeight)); + NSScreen *screenWindowIsOn = [NSScreen mainScreen]; + float greatestArea = 0; + + while (NSScreen *screen = [screenEnum nextObject]) { + NSDictionary *desc = [screen deviceDescription]; + if ([desc objectForKey:NSDeviceIsScreen] == nil) + continue; + + NSRect r = NSIntersectionRect([screen frame], inRect); + float area = r.size.width * r.size.height; + if (area > greatestArea) { + greatestArea = area; + screenWindowIsOn = screen; + } + } + + *outScreen = ScreenForCocoaScreen(screenWindowIsOn); + NS_ADDREF(*outScreen); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenManagerCocoa::GetPrimaryScreen (nsIScreen **outScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // the mainScreen is the screen with the "key window" (focus, I assume?) + NSScreen *sc = [[NSScreen screens] objectAtIndex:0]; + + *outScreen = ScreenForCocoaScreen(sc); + NS_ADDREF(*outScreen); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenManagerCocoa::GetNumberOfScreens (uint32_t *aNumberOfScreens) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSArray *ss = [NSScreen screens]; + + *aNumberOfScreens = [ss count]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsScreenManagerCocoa::GetSystemDefaultScale(float *aDefaultScale) +{ + *aDefaultScale = 1.0f; + return NS_OK; +} + +NS_IMETHODIMP +nsScreenManagerCocoa::ScreenForNativeWidget (void *nativeWidget, nsIScreen **outScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSWindow *window = static_cast<NSWindow*>(nativeWidget); + if (window) { + nsIScreen *screen = ScreenForCocoaScreen([window screen]); + *outScreen = screen; + NS_ADDREF(*outScreen); + return NS_OK; + } + + *outScreen = nullptr; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsSound.h b/widget/cocoa/nsSound.h new file mode 100644 index 000000000..0e0293ae2 --- /dev/null +++ b/widget/cocoa/nsSound.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 000000000..04c6b4d76 --- /dev/null +++ b/widget/cocoa/nsSound.mm @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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_ABORT_BLOCK_NSRESULT; + + NSBeep(); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsSound::OnStreamComplete(nsIStreamLoader *aLoader, + nsISupports *context, + nsresult aStatus, + uint32_t dataLen, + const uint8_t *data) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NSData *value = [NSData dataWithBytes:data length:dataLen]; + + NSSound *sound = [[NSSound alloc] initWithData:value]; + + [sound play]; + + [sound autorelease]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsSound::Play(nsIURL *aURL) +{ + nsCOMPtr<nsIURI> uri(do_QueryInterface(aURL)); + nsCOMPtr<nsIStreamLoader> loader; + return NS_NewStreamLoader(getter_AddRefs(loader), + uri, + this, // aObserver + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); +} + +NS_IMETHODIMP +nsSound::Init() +{ + return NS_OK; +} + +NS_IMETHODIMP +nsSound::PlaySystemSound(const nsAString &aSoundAlias) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (NS_IsMozAliasSound(aSoundAlias)) { + NS_WARNING("nsISound::playSystemSound is called with \"_moz_\" events, they are obsolete, use nsISound::playEventSound instead"); + // Mac doesn't have system sound settings for each user actions. + return NS_OK; + } + + NSString *name = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aSoundAlias.BeginReading()) + length:aSoundAlias.Length()]; + NSSound *sound = [NSSound soundNamed:name]; + if (sound) { + [sound stop]; + [sound play]; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +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 000000000..e03742b1e --- /dev/null +++ b/widget/cocoa/nsStandaloneNativeMenu.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 nsStandaloneNativeMenu_h_ +#define nsStandaloneNativeMenu_h_ + +#include "nsMenuGroupOwnerX.h" +#include "nsMenuX.h" +#include "nsIStandaloneNativeMenu.h" + +class nsStandaloneNativeMenu : public nsMenuGroupOwnerX, public nsIStandaloneNativeMenu +{ +public: + nsStandaloneNativeMenu(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSISTANDALONENATIVEMENU + + // nsMenuObjectX + nsMenuObjectTypeX MenuObjectType() override { return eStandaloneNativeMenuObjectType; } + void * NativeData() override { return mMenu != nullptr ? mMenu->NativeData() : nullptr; } + virtual void IconUpdated() override; + + nsMenuX * GetMenuXObject() { return mMenu; } + + // 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 ~nsStandaloneNativeMenu(); + + nsMenuX * mMenu; + NSStatusItem* mContainerStatusBarItem; +}; + +#endif diff --git a/widget/cocoa/nsStandaloneNativeMenu.mm b/widget/cocoa/nsStandaloneNativeMenu.mm new file mode 100644 index 000000000..98a5fd8f6 --- /dev/null +++ b/widget/cocoa/nsStandaloneNativeMenu.mm @@ -0,0 +1,213 @@ +/* -*- 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 "nsMenuUtilsX.h" +#include "nsIDOMElement.h" +#include "nsIMutationObserver.h" +#include "nsGkAtoms.h" +#include "nsObjCExceptions.h" + + +NS_IMPL_ISUPPORTS_INHERITED(nsStandaloneNativeMenu, nsMenuGroupOwnerX, + nsIMutationObserver, nsIStandaloneNativeMenu) + +nsStandaloneNativeMenu::nsStandaloneNativeMenu() +: mMenu(nullptr) +, mContainerStatusBarItem(nil) +{ +} + +nsStandaloneNativeMenu::~nsStandaloneNativeMenu() +{ + if (mMenu) delete mMenu; +} + +NS_IMETHODIMP +nsStandaloneNativeMenu::Init(nsIDOMElement * aDOMElement) +{ + NS_ASSERTION(mMenu == nullptr, "nsNativeMenu::Init - mMenu not null!"); + + nsresult rv; + + nsCOMPtr<nsIContent> content = do_QueryInterface(aDOMElement, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (!content->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup)) + return NS_ERROR_FAILURE; + + rv = nsMenuGroupOwnerX::Create(content); + if (NS_FAILED(rv)) + return rv; + + mMenu = new nsMenuX(); + rv = mMenu->Create(this, this, content); + if (NS_FAILED(rv)) { + delete mMenu; + mMenu = nullptr; + return rv; + } + + mMenu->SetupIcon(); + + return NS_OK; +} + +static void +UpdateMenu(nsMenuX * aMenu) +{ + aMenu->MenuOpened(); + aMenu->MenuClosed(); + + uint32_t itemCount = aMenu->GetItemCount(); + for (uint32_t i = 0; i < itemCount; i++) { + nsMenuObjectX * menuObject = aMenu->GetItemAt(i); + if (menuObject->MenuObjectType() == eSubmenuObjectType) { + UpdateMenu(static_cast<nsMenuX*>(menuObject)); + } + } +} + +NS_IMETHODIMP +nsStandaloneNativeMenu::MenuWillOpen(bool * aResult) +{ + NS_ASSERTION(mMenu != nullptr, "nsStandaloneNativeMenu::OnOpen - mMenu is null!"); + + // Force an update on the mMenu by faking an open/close on all of + // its submenus. + UpdateMenu(mMenu); + + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +nsStandaloneNativeMenu::GetNativeMenu(void ** aVoidPointer) +{ + if (mMenu) { + *aVoidPointer = mMenu->NativeData(); + [[(NSObject *)(*aVoidPointer) retain] autorelease]; + return NS_OK; + } else { + *aVoidPointer = nullptr; + return NS_ERROR_NOT_INITIALIZED; + } +} + +static NSMenuItem * +NativeMenuItemWithLocation(NSMenu * currentSubmenu, NSString * locationString) +{ + NSArray * indexes = [locationString componentsSeparatedByString:@"|"]; + NSUInteger indexCount = [indexes count]; + if (indexCount == 0) + return nil; + + for (NSUInteger i = 0; i < indexCount; i++) { + NSInteger targetIndex = [[indexes objectAtIndex:i] integerValue]; + NSInteger itemCount = [currentSubmenu numberOfItems]; + if (targetIndex < itemCount) { + NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex]; + + // If this is the last index, just return the menu item. + if (i == (indexCount - 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; +} + +NS_IMETHODIMP +nsStandaloneNativeMenu::ActivateNativeMenuItemAt(const nsAString& indexString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mMenu) + return NS_ERROR_NOT_INITIALIZED; + + NSString * locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading()) + length:indexString.Length()]; + NSMenu * menu = static_cast<NSMenu *> (mMenu->NativeData()); + NSMenuItem * item = NativeMenuItemWithLocation(menu, locationString); + + // 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]); + [parent performActionForItemAtIndex:[parent indexOfItem:item]]; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsStandaloneNativeMenu::ForceUpdateNativeMenuAt(const nsAString& indexString) +{ + if (!mMenu) + return NS_ERROR_NOT_INITIALIZED; + + NSString* locationString = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(indexString.BeginReading()) + length:indexString.Length()]; + NSArray* indexes = [locationString componentsSeparatedByString:@"|"]; + unsigned int indexCount = [indexes count]; + if (indexCount == 0) + return NS_OK; + + nsMenuX* currentMenu = mMenu; + + // now find the correct submenu + 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++) { + nsMenuObjectX* targetMenu = currentMenu->GetItemAt(j); + if (!targetMenu) + return NS_OK; + if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(targetMenu->Content())) { + visible++; + if (targetMenu->MenuObjectType() == eSubmenuObjectType && visible == (targetIndex + 1)) { + currentMenu = static_cast<nsMenuX*>(targetMenu); + // fake open/close to cause lazy update to happen + currentMenu->MenuOpened(); + currentMenu->MenuClosed(); + break; + } + } + } + } + + return NS_OK; +} + +void +nsStandaloneNativeMenu::IconUpdated() +{ + if (mContainerStatusBarItem) { + [mContainerStatusBarItem setImage:[mMenu->NativeMenuItem() image]]; + } +} + +void +nsStandaloneNativeMenu::SetContainerStatusBarItem(NSStatusItem* aItem) +{ + mContainerStatusBarItem = aItem; + IconUpdated(); +} diff --git a/widget/cocoa/nsSystemStatusBarCocoa.h b/widget/cocoa/nsSystemStatusBarCocoa.h new file mode 100644 index 000000000..51aa4df00 --- /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" + +class nsStandaloneNativeMenu; +@class NSStatusItem; + +class nsSystemStatusBarCocoa : public nsISystemStatusBar +{ +public: + nsSystemStatusBarCocoa() {} + + NS_DECL_ISUPPORTS + NS_DECL_NSISYSTEMSTATUSBAR + +protected: + virtual ~nsSystemStatusBarCocoa() {} + + struct StatusItem + { + explicit StatusItem(nsStandaloneNativeMenu* aMenu); + ~StatusItem(); + + private: + RefPtr<nsStandaloneNativeMenu> 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 000000000..522da7145 --- /dev/null +++ b/widget/cocoa/nsSystemStatusBarCocoa.mm @@ -0,0 +1,74 @@ +/* -*- 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 "nsStandaloneNativeMenu.h" +#include "nsObjCExceptions.h" +#include "nsIDOMElement.h" + +NS_IMPL_ISUPPORTS(nsSystemStatusBarCocoa, nsISystemStatusBar) + +NS_IMETHODIMP +nsSystemStatusBarCocoa::AddItem(nsIDOMElement* aDOMElement) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + RefPtr<nsStandaloneNativeMenu> menu = new nsStandaloneNativeMenu(); + nsresult rv = menu->Init(aDOMElement); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsISupports> keyPtr = aDOMElement; + mItems.Put(keyPtr, new StatusItem(menu)); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsSystemStatusBarCocoa::RemoveItem(nsIDOMElement* aDOMElement) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + mItems.Remove(aDOMElement); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsSystemStatusBarCocoa::StatusItem::StatusItem(nsStandaloneNativeMenu* aMenu) + : mMenu(aMenu) +{ + MOZ_COUNT_CTOR(nsSystemStatusBarCocoa::StatusItem); + + NSMenu* nativeMenu = nil; + mMenu->GetNativeMenu(reinterpret_cast<void**>(&nativeMenu)); + + mStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain]; + [mStatusItem setMenu:nativeMenu]; + [mStatusItem setHighlightMode: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); +} + +nsSystemStatusBarCocoa::StatusItem::~StatusItem() +{ + mMenu->SetContainerStatusBarItem(nil); + [[NSStatusBar systemStatusBar] removeStatusItem:mStatusItem]; + [mStatusItem release]; + mStatusItem = nil; + + MOZ_COUNT_DTOR(nsSystemStatusBarCocoa::StatusItem); +} diff --git a/widget/cocoa/nsToolkit.h b/widget/cocoa/nsToolkit.h new file mode 100644 index 000000000..1631a8ac2 --- /dev/null +++ b/widget/cocoa/nsToolkit.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef 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 RegisterForAllProcessMouseEvents(); + void UnregisterAllProcessMouseEventHandlers(); + +protected: + + nsresult RegisterForSleepWakeNotifications(); + void RemoveSleepWakeNotifications(); + +protected: + + static nsToolkit* gToolkit; + + CFRunLoopSourceRef mSleepWakeNotificationRLS; + io_object_t mPowerNotifier; + + CFMachPortRef mEventTapPort; + CFRunLoopSourceRef mEventTapRLS; +}; + +#endif // nsToolkit_h_ diff --git a/widget/cocoa/nsToolkit.mm b/widget/cocoa/nsToolkit.mm new file mode 100644 index 000000000..4d0222d5d --- /dev/null +++ b/widget/cocoa/nsToolkit.mm @@ -0,0 +1,326 @@ +/* -*- 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 "nsIServiceManager.h" + +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" + +using namespace mozilla; + +static io_connect_t gRootPort = MACH_PORT_NULL; + +nsToolkit* nsToolkit::gToolkit = nullptr; + +nsToolkit::nsToolkit() +: mSleepWakeNotificationRLS(nullptr) +, mEventTapPort(nullptr) +, mEventTapRLS(nullptr) +{ + MOZ_COUNT_CTOR(nsToolkit); + RegisterForSleepWakeNotifications(); +} + +nsToolkit::~nsToolkit() +{ + MOZ_COUNT_DTOR(nsToolkit); + RemoveSleepWakeNotifications(); + UnregisterAllProcessMouseEventHandlers(); +} + +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_ABORT_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_ABORT_BLOCK; +} + +nsresult +nsToolkit::RegisterForSleepWakeNotifications() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} + +void +nsToolkit::RemoveSleepWakeNotifications() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mSleepWakeNotificationRLS) { + ::IODeregisterForSystemPower(&mPowerNotifier); + ::CFRunLoopRemoveSource(::CFRunLoopGetCurrent(), + mSleepWakeNotificationRLS, + kCFRunLoopDefaultMode); + + mSleepWakeNotificationRLS = nullptr; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Converts aPoint from the CoreGraphics "global display coordinate" system +// (which includes all displays/screens and has a top-left origin) to its +// (presumed) Cocoa counterpart (assumed to be the same as the "screen +// coordinates" system), which has a bottom-left origin. +static NSPoint ConvertCGGlobalToCocoaScreen(CGPoint aPoint) +{ + NSPoint cocoaPoint; + cocoaPoint.x = aPoint.x; + cocoaPoint.y = nsCocoaUtils::FlippedScreenY(aPoint.y); + return cocoaPoint; +} + +// Since our event tap is "listen only", events arrive here a little after +// they've already been processed. +static CGEventRef EventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if ((type == kCGEventTapDisabledByUserInput) || + (type == kCGEventTapDisabledByTimeout)) + return event; + if ([NSApp isActive]) + return event; + + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + NS_ENSURE_TRUE(rollupListener, event); + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (!rollupWidget) + return event; + + // Don't bother with rightMouseDown events here -- because of the delay, + // we'll end up closing browser context menus that we just opened. Since + // these events usually raise a context menu, we'll handle them by hooking + // the @"com.apple.HIToolbox.beginMenuTrackingNotification" distributed + // notification (in nsAppShell.mm's AppShellDelegate). + if (type == kCGEventRightMouseDown) + return event; + NSWindow *ctxMenuWindow = (NSWindow*) rollupWidget->GetNativeData(NS_NATIVE_WINDOW); + if (!ctxMenuWindow) + return event; + NSPoint screenLocation = ConvertCGGlobalToCocoaScreen(CGEventGetLocation(event)); + // Don't roll up the rollup widget if our mouseDown happens over it (doing + // so would break the corresponding context menu). + if (NSPointInRect(screenLocation, [ctxMenuWindow frame])) + return event; + rollupListener->Rollup(0, false, nullptr, nullptr); + return event; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NULL); +} + +// 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::RegisterForAllProcessMouseEvents() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (getenv("MOZ_DEBUG")) + return; + + // Don't do this for apps that use native context menus. +#ifdef MOZ_USE_NATIVE_POPUP_WINDOWS + return; +#endif /* MOZ_USE_NATIVE_POPUP_WINDOWS */ + + if (!mEventTapRLS) { + // Using an event tap for mouseDown events (instead of installing a + // handler for them on the EventMonitor target) works around an Apple + // bug that causes OS menus (like the Clock menu) not to work properly + // on OS X 10.4.X and below (bmo bug 381448). + // We install our event tap "listen only" to get around yet another Apple + // bug -- when we install it as an event filter on any kind of mouseDown + // event, that kind of event stops working in the main menu, and usually + // mouse event processing stops working in all apps in the current login + // session (so the entire OS appears to be hung)! The downside of + // installing listen-only is that events arrive at our handler slightly + // after they've already been processed. + mEventTapPort = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionListenOnly, + CGEventMaskBit(kCGEventLeftMouseDown) + | CGEventMaskBit(kCGEventRightMouseDown) + | CGEventMaskBit(kCGEventOtherMouseDown), + EventTapCallback, + nullptr); + if (!mEventTapPort) + return; + mEventTapRLS = CFMachPortCreateRunLoopSource(nullptr, mEventTapPort, 0); + if (!mEventTapRLS) { + CFRelease(mEventTapPort); + mEventTapPort = nullptr; + return; + } + CFRunLoopAddSource(CFRunLoopGetCurrent(), mEventTapRLS, kCFRunLoopDefaultMode); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsToolkit::UnregisterAllProcessMouseEventHandlers() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mEventTapRLS) { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mEventTapRLS, + kCFRunLoopDefaultMode); + CFRelease(mEventTapRLS); + mEventTapRLS = nullptr; + } + if (mEventTapPort) { + // mEventTapPort must be invalidated as well as released. Otherwise the + // event tap doesn't get destroyed until the browser process ends (it + // keeps showing up in the list returned by CGGetEventTapList()). + CFMachPortInvalidate(mEventTapPort); + CFRelease(mEventTapPort); + mEventTapPort = nullptr; + } + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK_RETURN; + + if (!gToolkit) { + gToolkit = new nsToolkit(); + } + + return gToolkit; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK_NSRESULT; + + 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_ABORT_BLOCK_NSRESULT; +} diff --git a/widget/cocoa/nsWidgetFactory.mm b/widget/cocoa/nsWidgetFactory.mm new file mode 100644 index 000000000..3bddaf95c --- /dev/null +++ b/widget/cocoa/nsWidgetFactory.mm @@ -0,0 +1,219 @@ +/* -*- 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 "nsIFactory.h" +#include "nsISupports.h" +#include "nsIComponentManager.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/WidgetUtils.h" + +#include "nsWidgetsCID.h" + +#include "nsChildView.h" +#include "nsCocoaWindow.h" +#include "nsAppShell.h" +#include "nsAppShellSingleton.h" +#include "nsFilePicker.h" +#include "nsColorPicker.h" + +#include "nsClipboard.h" +#include "nsClipboardHelper.h" +#include "nsTransferable.h" +#include "nsHTMLFormatConverter.h" +#include "nsDragService.h" +#include "nsToolkit.h" + +#include "nsLookAndFeel.h" + +#include "nsSound.h" +#include "nsIdleServiceX.h" +#include "NativeKeyBindings.h" +#include "OSXNotificationCenter.h" + +#include "nsScreenManagerCocoa.h" +#include "nsDeviceContextSpecX.h" +#include "nsPrintOptionsX.h" +#include "nsPrintDialogX.h" +#include "nsPrintSession.h" +#include "nsToolkitCompsCID.h" + +using namespace mozilla; +using namespace mozilla::widget; + +NS_GENERIC_FACTORY_CONSTRUCTOR(nsCocoaWindow) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsChildView) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsFilePicker) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsColorPicker) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsSound) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsTransferable) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsHTMLFormatConverter) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsClipboard) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsClipboardHelper) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsDragService) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsScreenManagerCocoa) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsDeviceContextSpecX) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsPrintOptionsX, Init) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsPrintDialogServiceX, Init) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsPrintSession, Init) +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsIdleServiceX, nsIdleServiceX::GetInstance) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(OSXNotificationCenter, Init) + +#include "nsMenuBarX.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsNativeMenuServiceX) + +#include "nsBidiKeyboard.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsBidiKeyboard) + +#include "nsNativeThemeCocoa.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsNativeThemeCocoa) + +#include "nsMacDockSupport.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacDockSupport) + +#include "nsMacWebAppUtils.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacWebAppUtils) + +#include "nsStandaloneNativeMenu.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsStandaloneNativeMenu) + +#include "nsSystemStatusBarCocoa.h" +NS_GENERIC_FACTORY_CONSTRUCTOR(nsSystemStatusBarCocoa) + +#include "GfxInfo.h" +namespace mozilla { +namespace widget { +// This constructor should really be shared with all platforms. +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(GfxInfo, Init) +} // namespace widget +} // namespace mozilla + +NS_DEFINE_NAMED_CID(NS_WINDOW_CID); +NS_DEFINE_NAMED_CID(NS_POPUP_CID); +NS_DEFINE_NAMED_CID(NS_CHILD_CID); +NS_DEFINE_NAMED_CID(NS_FILEPICKER_CID); +NS_DEFINE_NAMED_CID(NS_COLORPICKER_CID); +NS_DEFINE_NAMED_CID(NS_APPSHELL_CID); +NS_DEFINE_NAMED_CID(NS_SOUND_CID); +NS_DEFINE_NAMED_CID(NS_TRANSFERABLE_CID); +NS_DEFINE_NAMED_CID(NS_HTMLFORMATCONVERTER_CID); +NS_DEFINE_NAMED_CID(NS_CLIPBOARD_CID); +NS_DEFINE_NAMED_CID(NS_CLIPBOARDHELPER_CID); +NS_DEFINE_NAMED_CID(NS_DRAGSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_BIDIKEYBOARD_CID); +NS_DEFINE_NAMED_CID(NS_THEMERENDERER_CID); +NS_DEFINE_NAMED_CID(NS_SCREENMANAGER_CID); +NS_DEFINE_NAMED_CID(NS_DEVICE_CONTEXT_SPEC_CID); +NS_DEFINE_NAMED_CID(NS_PRINTSESSION_CID); +NS_DEFINE_NAMED_CID(NS_PRINTSETTINGSSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_PRINTDIALOGSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_IDLE_SERVICE_CID); +NS_DEFINE_NAMED_CID(NS_SYSTEMALERTSSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_NATIVEMENUSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_MACDOCKSUPPORT_CID); +NS_DEFINE_NAMED_CID(NS_MACWEBAPPUTILS_CID); +NS_DEFINE_NAMED_CID(NS_STANDALONENATIVEMENU_CID); +NS_DEFINE_NAMED_CID(NS_MACSYSTEMSTATUSBAR_CID); +NS_DEFINE_NAMED_CID(NS_GFXINFO_CID); + +static const mozilla::Module::CIDEntry kWidgetCIDs[] = { + { &kNS_WINDOW_CID, false, NULL, nsCocoaWindowConstructor }, + { &kNS_POPUP_CID, false, NULL, nsCocoaWindowConstructor }, + { &kNS_CHILD_CID, false, NULL, nsChildViewConstructor }, + { &kNS_FILEPICKER_CID, false, NULL, nsFilePickerConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_COLORPICKER_CID, false, NULL, nsColorPickerConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_APPSHELL_CID, false, NULL, nsAppShellConstructor, mozilla::Module::ALLOW_IN_GPU_PROCESS }, + { &kNS_SOUND_CID, false, NULL, nsSoundConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_TRANSFERABLE_CID, false, NULL, nsTransferableConstructor }, + { &kNS_HTMLFORMATCONVERTER_CID, false, NULL, nsHTMLFormatConverterConstructor }, + { &kNS_CLIPBOARD_CID, false, NULL, nsClipboardConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_CLIPBOARDHELPER_CID, false, NULL, nsClipboardHelperConstructor }, + { &kNS_DRAGSERVICE_CID, false, NULL, nsDragServiceConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_BIDIKEYBOARD_CID, false, NULL, nsBidiKeyboardConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_THEMERENDERER_CID, false, NULL, nsNativeThemeCocoaConstructor }, + { &kNS_SCREENMANAGER_CID, false, NULL, nsScreenManagerCocoaConstructor, + mozilla::Module::MAIN_PROCESS_ONLY }, + { &kNS_DEVICE_CONTEXT_SPEC_CID, false, NULL, nsDeviceContextSpecXConstructor }, + { &kNS_PRINTSESSION_CID, false, NULL, nsPrintSessionConstructor }, + { &kNS_PRINTSETTINGSSERVICE_CID, false, NULL, nsPrintOptionsXConstructor }, + { &kNS_PRINTDIALOGSERVICE_CID, false, NULL, nsPrintDialogServiceXConstructor }, + { &kNS_IDLE_SERVICE_CID, false, NULL, nsIdleServiceXConstructor }, + { &kNS_SYSTEMALERTSSERVICE_CID, false, NULL, OSXNotificationCenterConstructor }, + { &kNS_NATIVEMENUSERVICE_CID, false, NULL, nsNativeMenuServiceXConstructor }, + { &kNS_MACDOCKSUPPORT_CID, false, NULL, nsMacDockSupportConstructor }, + { &kNS_MACWEBAPPUTILS_CID, false, NULL, nsMacWebAppUtilsConstructor }, + { &kNS_STANDALONENATIVEMENU_CID, false, NULL, nsStandaloneNativeMenuConstructor }, + { &kNS_MACSYSTEMSTATUSBAR_CID, false, NULL, nsSystemStatusBarCocoaConstructor }, + { &kNS_GFXINFO_CID, false, NULL, mozilla::widget::GfxInfoConstructor }, + { NULL } +}; + +static const mozilla::Module::ContractIDEntry kWidgetContracts[] = { + { "@mozilla.org/widgets/window/mac;1", &kNS_WINDOW_CID }, + { "@mozilla.org/widgets/popup/mac;1", &kNS_POPUP_CID }, + { "@mozilla.org/widgets/childwindow/mac;1", &kNS_CHILD_CID }, + { "@mozilla.org/filepicker;1", &kNS_FILEPICKER_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/colorpicker;1", &kNS_COLORPICKER_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/widget/appshell/mac;1", &kNS_APPSHELL_CID, mozilla::Module::ALLOW_IN_GPU_PROCESS }, + { "@mozilla.org/sound;1", &kNS_SOUND_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/widget/transferable;1", &kNS_TRANSFERABLE_CID }, + { "@mozilla.org/widget/htmlformatconverter;1", &kNS_HTMLFORMATCONVERTER_CID }, + { "@mozilla.org/widget/clipboard;1", &kNS_CLIPBOARD_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/widget/clipboardhelper;1", &kNS_CLIPBOARDHELPER_CID }, + { "@mozilla.org/widget/dragservice;1", &kNS_DRAGSERVICE_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/widget/bidikeyboard;1", &kNS_BIDIKEYBOARD_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/chrome/chrome-native-theme;1", &kNS_THEMERENDERER_CID }, + { "@mozilla.org/gfx/screenmanager;1", &kNS_SCREENMANAGER_CID, + mozilla::Module::MAIN_PROCESS_ONLY }, + { "@mozilla.org/gfx/devicecontextspec;1", &kNS_DEVICE_CONTEXT_SPEC_CID }, + { "@mozilla.org/gfx/printsession;1", &kNS_PRINTSESSION_CID }, + { "@mozilla.org/gfx/printsettings-service;1", &kNS_PRINTSETTINGSSERVICE_CID }, + { NS_PRINTDIALOGSERVICE_CONTRACTID, &kNS_PRINTDIALOGSERVICE_CID }, + { "@mozilla.org/widget/idleservice;1", &kNS_IDLE_SERVICE_CID }, + { "@mozilla.org/system-alerts-service;1", &kNS_SYSTEMALERTSSERVICE_CID }, + { "@mozilla.org/widget/nativemenuservice;1", &kNS_NATIVEMENUSERVICE_CID }, + { "@mozilla.org/widget/macdocksupport;1", &kNS_MACDOCKSUPPORT_CID }, + { "@mozilla.org/widget/mac-web-app-utils;1", &kNS_MACWEBAPPUTILS_CID }, + { "@mozilla.org/widget/standalonenativemenu;1", &kNS_STANDALONENATIVEMENU_CID }, + { "@mozilla.org/widget/macsystemstatusbar;1", &kNS_MACSYSTEMSTATUSBAR_CID }, + { "@mozilla.org/gfx/info;1", &kNS_GFXINFO_CID }, + { NULL } +}; + +static void +nsWidgetCocoaModuleDtor() +{ + // Shutdown all XP level widget classes. + WidgetUtils::Shutdown(); + + NativeKeyBindings::Shutdown(); + nsLookAndFeel::Shutdown(); + nsToolkit::Shutdown(); + nsAppShellShutdown(); +} + +static const mozilla::Module kWidgetModule = { + mozilla::Module::kVersion, + kWidgetCIDs, + kWidgetContracts, + NULL, + NULL, + nsAppShellInit, + nsWidgetCocoaModuleDtor, + mozilla::Module::ALLOW_IN_GPU_PROCESS +}; + +NSMODULE_DEFN(nsWidgetMacModule) = &kWidgetModule; diff --git a/widget/cocoa/nsWindowMap.h b/widget/cocoa/nsWindowMap.h new file mode 100644 index 000000000..c6ad72c01 --- /dev/null +++ b/widget/cocoa/nsWindowMap.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 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 000000000..c43b02408 --- /dev/null +++ b/widget/cocoa/nsWindowMap.mm @@ -0,0 +1,311 @@ +/* -*- 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_ABORT_BLOCK_NIL; + + static WindowDataMap* sWindowMap = nil; + if (!sWindowMap) + sWindowMap = [[WindowDataMap alloc] init]; + + return sWindowMap; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)init +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ((self = [super init])) { + mWindowMap = [[NSMutableDictionary alloc] initWithCapacity:10]; + } + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindowMap release]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)ensureDataForWindow:(NSWindow*)inWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK; +} + +- (id)dataForWindow:(NSWindow*)inWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [mWindowMap objectForKey:[self keyForWindow:inWindow]]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)setData:(id)inData forWindow:(NSWindow*)inWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindowMap setObject:inData forKey:[self keyForWindow:inWindow]]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)removeDataForWindow:(NSWindow*)inWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindowMap removeObjectForKey:[self keyForWindow:inWindow]]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (NSString*)keyForWindow:(NSWindow*)inWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [NSString stringWithFormat:@"%p", inWindow]; + + NS_OBJC_END_TRY_ABORT_BLOCK_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_ABORT_BLOCK_NIL; + + 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_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK; + + WindowDelegate* delegate = (WindowDelegate*) [aWindow delegate]; + if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) + return; + + if ([delegate toplevelActiveState]) + return; + [delegate sendToplevelActivateEvents]; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK; + + WindowDelegate* delegate = (WindowDelegate*) [aWindow delegate]; + if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) + return; + + if (![delegate toplevelActiveState]) + return; + [delegate sendToplevelDeactivateEvents]; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK; + + id firstResponder = [aWindow firstResponder]; + if ([firstResponder isKindOfClass:[ChildView class]]) + [firstResponder viewsWindowDidBecomeKey]; + + NS_OBJC_END_TRY_ABORT_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_ABORT_BLOCK; + + id firstResponder = [aWindow firstResponder]; + if ([firstResponder isKindOfClass:[ChildView class]]) + [firstResponder viewsWindowDidResignKey]; + + NS_OBJC_END_TRY_ABORT_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]; + } + + [[window contentView] setNeedsDisplay:YES]; +} + +- (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]; + } + + [[window contentView] setNeedsDisplay:YES]; +} + +// 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_ABORT_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_ABORT_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 000000000..b9b4b09f6 --- /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 000000000..bcf3ace84 --- /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 000000000..16b3f7e52 --- /dev/null +++ b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib |