Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  nsCocoaWindow.mm   Sprache: unbekannt

 
/* -*- Mode: 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 "ScreenHelperCocoa.h"
#include "TextInputHandler.h"
#include "nsCocoaUtils.h"
#include "nsObjCExceptions.h"
#include "nsCOMPtr.h"
#include "nsWidgetsCID.h"
#include "nsIRollupListener.h"
#include "nsChildView.h"
#include "nsWindowMap.h"
#include "nsAppShell.h"
#include "nsIAppShellService.h"
#include "nsIBaseWindow.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIAppWindow.h"
#include "nsToolkit.h"
#include "nsPIDOMWindow.h"
#include "nsThreadUtils.h"
#include "nsMenuBarX.h"
#include "nsMenuUtilsX.h"
#include "nsStyleConsts.h"
#include "nsNativeThemeColors.h"
#include "nsNativeThemeCocoa.h"
#include "nsChildView.h"
#include "nsCocoaFeatures.h"
#include "nsIScreenManager.h"
#include "nsIWidgetListener.h"
#include "nsXULPopupManager.h"
#include "VibrancyManager.h"
#include "nsPresContext.h"
#include "nsDocShell.h"

#include "gfxPlatform.h"
#include "qcms.h"

#include "mozilla/AutoRestore.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/dom/Document.h"
#include "mozilla/Maybe.h"
#include "mozilla/NativeKeyBindingsType.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_gfx.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/WritingModes.h"
#include "mozilla/layers/CompositorBridgeChild.h"
#include "mozilla/widget/Screen.h"
#include <algorithm>

namespace mozilla {
namespace layers {
class LayerManager;
}  // namespace layers
}  // namespace mozilla
using namespace mozilla::layers;
using namespace mozilla::widget;
using namespace mozilla;

BOOL sTouchBarIsInitialized = NO;

// defined in nsMenuBarX.mm
extern NSMenu* sApplicationMenu;  // Application menu shared by all menubars

// defined in nsChildView.mm
extern BOOL gSomeMenuBarPainted;

static uint32_t sModalWindowCount = 0;

extern "C" {
// CGSPrivate.h
typedef NSInteger CGSConnection;
typedef NSUInteger CGSSpaceID;
typedef NSInteger CGSWindow;
typedef enum {
  kCGSSpaceIncludesCurrent = 1 << 0,
  kCGSSpaceIncludesOthers = 1 << 1,
  kCGSSpaceIncludesUser = 1 << 2,

  kCGSAllSpacesMask =
      kCGSSpaceIncludesCurrent | kCGSSpaceIncludesOthers | kCGSSpaceIncludesUser
} CGSSpaceMask;
static NSString* const CGSSpaceIDKey = @"ManagedSpaceID";
static NSString* const CGSSpacesKey = @"Spaces";
extern CGSConnection _CGSDefaultConnection(void);
extern CGError CGSSetWindowTransform(CGSConnection cid, CGSWindow wid,
                                     CGAffineTransform transform);
}

#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"

static void RollUpPopups(nsIRollupListener::AllowAnimations aAllowAnimations =
                             nsIRollupListener::AllowAnimations::Yes) {
  if (RefPtr pm = nsXULPopupManager::GetInstance()) {
    pm->RollupTooltips();
  }

  nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
  if (!rollupListener) {
    return;
  }
  if (rollupListener->RollupNativeMenu()) {
    return;
  }
  nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
  if (!rollupWidget) {
    return;
  }
  nsIRollupListener::RollupOptions options{
      0, nsIRollupListener::FlushViews::Yes, nullptr, aAllowAnimations};
  rollupListener->Rollup(options);
}

nsCocoaWindow::nsCocoaWindow()
    : mWindow(nil),
      mClosedRetainedWindow(nil),
      mDelegate(nil),
      mPopupContentView(nil),
      mFullscreenTransitionAnimation(nil),
      mShadowStyle(WindowShadow::None),
      mBackingScaleFactor(0.0),
      mAnimationType(nsIWidget::eGenericWindowAnimation),
      mWindowMadeHere(false),
      mSizeMode(nsSizeMode_Normal),
      mInFullScreenMode(false),
      mInNativeFullScreenMode(false),
      mIgnoreOcclusionCount(0),
      mHasStartedNativeFullscreen(false),
      mWindowAnimationBehavior(NSWindowAnimationBehaviorDefault) {
  // Disable automatic tabbing. We need to do this before we
  // orderFront any of our windows.
  NSWindow.allowsAutomaticWindowTabbing = NO;
}

void nsCocoaWindow::DestroyNativeWindow() {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow) {
    return;
  }

  MOZ_ASSERT(mWindowMadeHere,
             "We shouldn't be trying to destroy a window we didn't create.");

  // Clear our class state that is keyed off of mWindow. It's our last
  // chance! This ensures that other nsCocoaWindow instances are not waiting
  // for us to finish a native transition that will have no listener once
  // we clear our delegate.
  EndOurNativeTransition();

  // We are about to destroy mWindow. Before we do that, make sure that we
  // hide the window using the Show() method, because it has several side
  // effects that our parent and listeners might be expecting. If we don't
  // do this now, then these side effects will never execute, though the
  // window will definitely no longer be shown.
  Show(false);

  [mWindow removeTrackingArea];

  [mWindow releaseJSObjects];
  // We want to unhook the delegate here because we don't want events
  // sent to it after this object has been destroyed.
  mWindow.delegate = nil;

  // Closing the window will also release it. Our second reference will
  // keep it alive through our destructor. Release any reference we might
  // have from an earlier call to DestroyNativeWindow, then create a new
  // one.
  [mClosedRetainedWindow autorelease];
  mClosedRetainedWindow = [mWindow retain];
  MOZ_ASSERT(mWindow.releasedWhenClosed);
  [mWindow close];

  mWindow = nil;
  [mDelegate autorelease];

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

nsCocoaWindow::~nsCocoaWindow() {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  RemoveAllChildren();
  if (mWindow && mWindowMadeHere) {
    CancelAllTransitions();
    DestroyNativeWindow();
  }

  [mClosedRetainedWindow release];

  NS_IF_RELEASE(mPopupContentView);
  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

// Find the screen that overlaps aRect the most,
// if none are found default to the mainScreen.
static NSScreen* FindTargetScreenForRect(const DesktopIntRect& aRect) {
  NSScreen* targetScreen = [NSScreen mainScreen];
  NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
  int largestIntersectArea = 0;
  while (NSScreen* screen = [screenEnum nextObject]) {
    DesktopIntRect screenRect =
        nsCocoaUtils::CocoaRectToGeckoRect([screen visibleFrame]);
    screenRect = screenRect.Intersect(aRect);
    int area = screenRect.width * screenRect.height;
    if (area > largestIntersectArea) {
      largestIntersectArea = area;
      targetScreen = screen;
    }
  }
  return targetScreen;
}

DesktopToLayoutDeviceScale ParentBackingScaleFactor(nsIWidget* aParent) {
  if (aParent) {
    return aParent->GetDesktopToDeviceScale();
  }
  return DesktopToLayoutDeviceScale(1.0);
}

// Returns the screen rectangle for the given widget.
// Child widgets are positioned relative to this rectangle.
// Exactly one of the arguments must be non-null.
static DesktopRect GetWidgetScreenRectForChildren(nsIWidget* aWidget) {
  mozilla::DesktopToLayoutDeviceScale scale =
      aWidget->GetDesktopToDeviceScale();
  if (aWidget->GetWindowType() == WindowType::Child) {
    return aWidget->GetScreenBounds() / scale;
  }
  return aWidget->GetClientBounds() / scale;
}

// aRect here is specified in desktop pixels
//
// For child windows aRect.{x,y} are offsets from the origin of the parent
// window and not an absolute position.
nsresult nsCocoaWindow::Create(nsIWidget* aParent, const DesktopIntRect& aRect,
                               widget::InitData* aInitData) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  // Because the hidden window is created outside of an event loop,
  // we have to provide an autorelease pool (see bug 559075).
  nsAutoreleasePool localPool;

  // Set defaults which can be overriden from aInitData in BaseCreate
  mWindowType = WindowType::TopLevel;
  mBorderStyle = BorderStyle::Default;

  // Ensure that the toolkit is created.
  nsToolkit::GetToolkit();

  Inherited::BaseCreate(aParent, aInitData);

  mAlwaysOnTop = aInitData->mAlwaysOnTop;
  mIsAlert = aInitData->mIsAlert;

  // If we have a parent widget, the new widget will be offset from the
  // parent widget by aRect.{x,y}. Otherwise, we'll use aRect for the
  // new widget coordinates.
  DesktopIntPoint parentOrigin;

  // Do we have a parent widget?
  if (aParent) {
    DesktopRect parentDesktopRect = GetWidgetScreenRectForChildren(aParent);
    parentOrigin = gfx::RoundedToInt(parentDesktopRect.TopLeft());
  }

  DesktopIntRect widgetRect = aRect + parentOrigin;

  nsresult rv =
      CreateNativeWindow(nsCocoaUtils::GeckoRectToCocoaRect(widgetRect),
                         mBorderStyle, false, aInitData->mIsPrivate);
  NS_ENSURE_SUCCESS(rv, rv);

  if (mWindowType == WindowType::Popup) {
    // now we can convert widgetRect to device pixels for the window we created,
    // as the child view expects a rect expressed in the dev pix of its parent
    LayoutDeviceIntRect devRect =
        RoundedToInt(aRect * GetDesktopToDeviceScale());
    return CreatePopupContentView(devRect, aInitData);
  }

  mIsAnimationSuppressed = aInitData->mIsAnimationSuppressed;

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

nsresult nsCocoaWindow::Create(nsIWidget* aParent,
                               const LayoutDeviceIntRect& aRect,
                               widget::InitData* aInitData) {
  DesktopIntRect desktopRect =
      RoundedToInt(aRect / ParentBackingScaleFactor(aParent));
  return Create(aParent, desktopRect, aInitData);
}

static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
  bool allOrDefault = (aBorderStyle == BorderStyle::All ||
                       aBorderStyle == BorderStyle::Default);

  /* Apple's docs on NSWindow styles say that "a window's style mask should
   * include NSWindowStyleMaskTitled if it includes any of the others [besides
   * NSWindowStyleMaskBorderless]".  This implies that a borderless window
   * shouldn't have any other styles than NSWindowStyleMaskBorderless.
   */
  if (!allOrDefault && !(aBorderStyle & BorderStyle::Title)) {
    if (aBorderStyle & BorderStyle::Minimize) {
      /* It appears that at a minimum, borderless windows can be miniaturizable,
       * effectively contradicting some of Apple's documentation referenced
       * above. One such exception is the screen share indicator, see
       * bug 1742877.
       */
      return NSWindowStyleMaskBorderless | NSWindowStyleMaskMiniaturizable;
    }
    return NSWindowStyleMaskBorderless;
  }

  unsigned int mask = NSWindowStyleMaskTitled;
  if (allOrDefault || aBorderStyle & BorderStyle::Close) {
    mask |= NSWindowStyleMaskClosable;
  }
  if (allOrDefault || aBorderStyle & BorderStyle::Minimize) {
    mask |= NSWindowStyleMaskMiniaturizable;
  }
  if (allOrDefault || aBorderStyle & BorderStyle::ResizeH) {
    mask |= NSWindowStyleMaskResizable;
  }

  return mask;
}

// If aRectIsFrameRect, aRect specifies the frame rect of the new window.
// Otherwise, aRect.x/y specify the position of the window's frame relative to
// the bottom of the menubar and aRect.width/height specify the size of the
// content rect.
nsresult nsCocoaWindow::CreateNativeWindow(const NSRect& aRect,
                                           BorderStyle aBorderStyle,
                                           bool aRectIsFrameRect,
                                           bool aIsPrivateBrowsing) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  // We default to NSWindowStyleMaskBorderless, add features if needed.
  unsigned int features = NSWindowStyleMaskBorderless;

  // Configure the window we will create based on the window type.
  switch (mWindowType) {
    case WindowType::Invisible:
    case WindowType::Child:
      break;
    case WindowType::Popup:
      if (aBorderStyle != BorderStyle::Default &&
          mBorderStyle & BorderStyle::Title) {
        features |= NSWindowStyleMaskTitled;
        if (aBorderStyle & BorderStyle::Close) {
          features |= NSWindowStyleMaskClosable;
        }
      }
      break;
    case WindowType::TopLevel:
    case WindowType::Dialog:
      features = WindowMaskForBorderStyle(aBorderStyle);
      break;
    default:
      NS_ERROR("Unhandled window type!");
      return NS_ERROR_FAILURE;
  }

  NSRect contentRect;

  if (aRectIsFrameRect) {
    contentRect = [NSWindow contentRectForFrameRect:aRect styleMask:features];
  } else {
    /*
     * We pass a content area rect to initialize the native Cocoa window. The
     * content rect we give is the same size as the size we're given by gecko.
     * The origin we're given for non-popup windows is moved down by the height
     * of the menu bar so that an origin of (0,100) from gecko puts the window
     * 100 pixels below the top of the available desktop area. We also move the
     * origin down by the height of a title bar if it exists. This is so the
     * origin that gecko gives us for the top-left of  the window turns out to
     * be the top-left of the window we create. This is how it was done in
     * Carbon. If it ought to be different we'll probably need to look at all
     * the callers.
     *
     * Note: This means that if you put a secondary screen on top of your main
     * screen and open a window in the top screen, it'll be incorrectly shifted
     * down by the height of the menu bar. Same thing would happen in Carbon.
     *
     * Note: If you pass a rect with 0,0 for an origin, the window ends up in a
     * weird place for some reason. This stops that without breaking popups.
     */
    // Compensate for difference between frame and content area height (e.g.
    // title bar).
    NSRect newWindowFrame = [NSWindow frameRectForContentRect:aRect
                                                    styleMask:features];

    contentRect = aRect;
    contentRect.origin.y -= (newWindowFrame.size.height - aRect.size.height);

    if (mWindowType != WindowType::Popup) {
      contentRect.origin.y -= NSApp.mainMenu.menuBarHeight;
    }
  }

  // NSLog(@"Top-level window being created at Cocoa rect: %f, %f, %f, %f\n",
  //       rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);

  Class windowClass = [BaseWindow class];
  if ((mWindowType == WindowType::TopLevel ||
       mWindowType == WindowType::Dialog) &&
      (features & NSWindowStyleMaskTitled)) {
    // 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.
    windowClass = [ToolbarWindow class];
  } else if (mWindowType == WindowType::Popup) {
    windowClass = [PopupWindow class];
    // If we're a popup window we need to use the PopupWindow class.
  } else if (features == NSWindowStyleMaskBorderless) {
    // If we're a non-popup borderless window we need to use the
    // BorderlessWindow class.
    windowClass = [BorderlessWindow class];
  }

  // Create the window
  mWindow = [[windowClass alloc] initWithContentRect:contentRect
                                           styleMask:features
                                             backing:NSBackingStoreBuffered
                                               defer:YES];

  // Make sure that window titles don't leak to disk in private browsing mode
  // due to macOS' resume feature.
  mWindow.restorable = !aIsPrivateBrowsing;
  if (aIsPrivateBrowsing) {
    [mWindow disableSnapshotRestoration];
  }

  // setup our notification delegate. Note that setDelegate: does NOT retain.
  mDelegate = [[WindowDelegate alloc] initWithGeckoWindow:this];
  mWindow.delegate = mDelegate;

  // Make sure that the content rect we gave has been honored.
  NSRect wantedFrame = [mWindow frameRectForChildViewRect:contentRect];
  if (!NSEqualRects(mWindow.frame, wantedFrame)) {
    // This can happen when the window is not on the primary screen.
    [mWindow setFrame:wantedFrame display:NO];
  }
  UpdateBounds();

  if (mWindowType == WindowType::Invisible) {
    mWindow.level = kCGDesktopWindowLevelKey;
  }

  if (mWindowType == WindowType::Popup) {
    SetPopupWindowLevel();
    mWindow.backgroundColor = NSColor.clearColor;
    mWindow.opaque = NO;

    // When multiple spaces are in use and the browser is assigned to a
    // particular space, override the "Assign To" space and display popups on
    // the active space. Does not work with multiple displays. See
    // NeedsRecreateToReshow() for multi-display with multi-space workaround.
    mWindow.collectionBehavior = mWindow.collectionBehavior |
                                 NSWindowCollectionBehaviorMoveToActiveSpace;
  } else {
    // Non-popup windows are always opaque.
    mWindow.opaque = YES;
  }

  if (mAlwaysOnTop || mIsAlert) {
    mWindow.level = NSFloatingWindowLevel;
    mWindow.collectionBehavior =
        mWindow.collectionBehavior | NSWindowCollectionBehaviorCanJoinAllSpaces;
  }
  mWindow.contentMinSize = NSMakeSize(60, 60);
  [mWindow disableCursorRects];

  // Make the window use CoreAnimation from the start, so that we don't
  // switch from a non-CA window to a CA-window in the middle.
  mWindow.contentView.wantsLayer = YES;

  [mWindow createTrackingArea];

  // Make sure the window starts out not draggable by the background.
  // We will turn it on as necessary.
  mWindow.movableByWindowBackground = NO;

  [WindowDataMap.sharedWindowDataMap ensureDataForWindow:mWindow];
  mWindowMadeHere = true;

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

nsresult nsCocoaWindow::CreatePopupContentView(const LayoutDeviceIntRect& aRect,
                                               widget::InitData* aInitData) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  // We need to make our content view a ChildView.
  mPopupContentView = new nsChildView();
  if (!mPopupContentView) return NS_ERROR_FAILURE;

  NS_ADDREF(mPopupContentView);

  nsIWidget* thisAsWidget = static_cast<nsIWidget*>(this);
  nsresult rv = mPopupContentView->Create(thisAsWidget, aRect, aInitData);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  NSView* contentView = mWindow.contentView;
  auto* childView = static_cast<ChildView*>(
      mPopupContentView->GetNativeData(NS_NATIVE_WIDGET));
  childView.frame = contentView.bounds;
  childView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
  [contentView addSubview:childView];

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

void nsCocoaWindow::Destroy() {
  if (mOnDestroyCalled) {
    return;
  }
  mOnDestroyCalled = true;

  // Deal with the possiblity that we're being destroyed while running modal.
  if (mModal) {
    SetModal(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();
  }

  if (mInFullScreenMode && !mInNativeFullScreenMode) {
    // Keep these calls balanced for emulated fullscreen.
    nsCocoaUtils::HideOSChromeOnScreen(false);
  }

  // Destroy the native window here (and not wait for that to happen in our
  // destructor). 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.
  if (mWindow && mWindowMadeHere) {
    CancelAllTransitions();
    DestroyNativeWindow();
  }

  nsCOMPtr<nsIWidget> kungFuDeathGrip(this);

  nsBaseWidget::OnDestroy();
  nsBaseWidget::Destroy();
}

void* nsCocoaWindow::GetNativeData(uint32_t aDataType) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  void* retVal = nullptr;

  switch (aDataType) {
    // to emulate how windows works, we always have to return a NSView
    // for NS_NATIVE_WIDGET
    case NS_NATIVE_WIDGET:
      retVal = mWindow.contentView;
      break;

    case NS_NATIVE_WINDOW:
      retVal = mWindow;
      break;

    case NS_NATIVE_GRAPHIC:
      // There isn't anything that makes sense to return here,
      // and it doesn't matter so just return nullptr.
      NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a top-level window!");
      break;
    case NS_RAW_NATIVE_IME_CONTEXT: {
      retVal = GetPseudoIMEContext();
      if (retVal) {
        break;
      }
      NSView* view = mWindow ? mWindow.contentView : nil;
      if (view) {
        retVal = view.inputContext;
      }
      // If inputContext isn't available on this window, return this window's
      // pointer instead of nullptr since if this returns nullptr,
      // IMEStateManager cannot manage composition with TextComposition
      // instance.  Although, this case shouldn't occur.
      if (NS_WARN_IF(!retVal)) {
        retVal = this;
      }
      break;
    }
  }

  return retVal;

  NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
}

bool nsCocoaWindow::IsVisible() const {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  return mWindow && mWindow.isVisibleOrBeingShown;

  NS_OBJC_END_TRY_BLOCK_RETURN(false);
}

void nsCocoaWindow::SetModal(bool aModal) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (mModal == aModal) {
    return;
  }

  // Unlike many functions here, we explicitly *do not check* for the
  // existence of mWindow. This is to ensure that calls to SetModal have
  // no early exits and always update state. That way, if the calls are
  // balanced, we get expected behavior even if the native window has
  // been destroyed during the modal period. Within this function, all
  // the calls to mWindow will resolve even if mWindow is nil (as is
  // guaranteed by Objective-C). And since those calls are only concerned
  // with changing mWindow appearance/level, it's fine for them to be
  // no-ops if mWindow has already been destroyed.

  // 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 = aModal;

  if (aModal) {
    sModalWindowCount++;
  } else {
    MOZ_ASSERT(sModalWindowCount);
    sModalWindowCount--;
  }

  // When a window gets "set modal", make the window(s) that it appears over
  // behave as they should.  We can't rely on native methods to do this, for the
  // following reason:  The OS runs modal non-sheet windows in an event loop
  // (using [NSApplication runModalForWindow:] or similar methods) that's
  // incompatible with the modal event loop in AppWindow::ShowModal() (each of
  // these event loops is "exclusive", and can't run at the same time as other
  // (similar) event loops).
  for (auto* ancestorWidget = mParent; ancestorWidget;
       ancestorWidget = ancestorWidget->GetParent()) {
    if (ancestorWidget->GetWindowType() == WindowType::Child) {
      continue;
    }
    auto* ancestor = static_cast<nsCocoaWindow*>(ancestorWidget);
    const bool changed = aModal ? ancestor->mNumModalDescendants++ == 0
                                : --ancestor->mNumModalDescendants == 0;
    NS_ASSERTION(ancestor->mNumModalDescendants >= 0,
                 "Widget hierarchy changed while modal!");
    if (!changed || ancestor->mWindowType == WindowType::Invisible) {
      continue;
    }
    NSWindow* win = ancestor->GetCocoaWindow();
    [[win standardWindowButton:NSWindowCloseButton] setEnabled:!aModal];
    [[win standardWindowButton:NSWindowMiniaturizeButton] setEnabled:!aModal];
    [[win standardWindowButton:NSWindowZoomButton] setEnabled:!aModal];
  }
  if (aModal) {
    mWindow.level = NSModalPanelWindowLevel;
  } else if (mWindowType == WindowType::Popup) {
    SetPopupWindowLevel();
  } else {
    mWindow.level = NSNormalWindowLevel;
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

bool nsCocoaWindow::IsRunningAppModal() { return [NSApp _isRunningAppModal]; }

// Hide or show this window
void nsCocoaWindow::Show(bool aState) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow) {
    return;
  }

  // Early exit if our current visibility state is already the requested
  // state.
  if (aState == mWindow.isVisibleOrBeingShown) {
    return;
  }

  [mWindow setBeingShown:aState];
  if (aState && !mWasShown) {
    mWasShown = true;
  }

  NSWindow* nativeParentWindow =
      mParent ? (NSWindow*)mParent->GetNativeData(NS_NATIVE_WINDOW) : nil;

  if (aState && !mBounds.IsEmpty()) {
    // If we had set the activationPolicy to accessory, then right now we won't
    // have a dock icon. Make sure that we undo that and show a dock icon now
    // that we're going to show a window.
    if (NSApp.activationPolicy != NSApplicationActivationPolicyRegular) {
      NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
      PR_SetEnv("MOZ_APP_NO_DOCK=");
    }

    // Don't try to show a popup when the parent isn't visible or is minimized.
    if (mWindowType == WindowType::Popup && nativeParentWindow) {
      if (!nativeParentWindow.isVisible || nativeParentWindow.isMiniaturized) {
        return;
      }
    }

    if (mPopupContentView) {
      // Ensure our content view is visible. We never need to hide it.
      mPopupContentView->Show(true);
    }

    // We're about to show a window. If we are opening the new window while the
    // user is in a fullscreen space, for example because the new window is
    // opened from an existing fullscreen window, then macOS will open the new
    // window in fullscreen, too. For some windows, this is not desirable. We
    // want to prevent it for any popup, alert, or alwaysOnTop windows that
    // aren't already in fullscreen. If the user already got the window into
    // fullscreen somehow, that's fine, but we don't want the initial display to
    // be in fullscreen.
    bool savedValueForSupportsNativeFullscreen = GetSupportsNativeFullscreen();
    if (!mInFullScreenMode &&
        ((mWindowType == WindowType::Popup) || mAlwaysOnTop || mIsAlert)) {
      SetSupportsNativeFullscreen(false);
    }

    if (mWindowType == WindowType::Popup) {
      // For reasons that aren't yet clear, calls to [NSWindow orderFront:] or
      // [NSWindow makeKeyAndOrderFront:] can sometimes trigger "Error (1000)
      // creating CGSWindow", which in turn triggers an internal inconsistency
      // NSException.  These errors shouldn't be fatal.  So we need to wrap
      // calls to ...orderFront: in TRY blocks.  See bmo bug 470864.
      NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
      [[mWindow contentView] setNeedsDisplay:YES];
      if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) {
        [mWindow orderFront:nil];
      }
      NS_OBJC_END_TRY_IGNORE_BLOCK;
      // 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.
      if (nativeParentWindow && mPopupLevel == PopupLevel::Parent) {
        [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
      }
    } else {
      NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
      if (mWindowType == WindowType::TopLevel &&
          [mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
        NSWindowAnimationBehavior behavior;
        if (mIsAnimationSuppressed) {
          behavior = NSWindowAnimationBehaviorNone;
        } else {
          switch (mAnimationType) {
            case nsIWidget::eDocumentWindowAnimation:
              behavior = NSWindowAnimationBehaviorDocumentWindow;
              break;
            default:
              MOZ_FALLTHROUGH_ASSERT("unexpected mAnimationType value");
            case nsIWidget::eGenericWindowAnimation:
              behavior = NSWindowAnimationBehaviorDefault;
              break;
          }
        }
        [mWindow setAnimationBehavior:behavior];
        mWindowAnimationBehavior = behavior;
      }

      // We don't want alwaysontop / alert windows to pull focus when they're
      // opened, as these tend to be for peripheral indicators and displays.
      if (mAlwaysOnTop || mIsAlert) {
        [mWindow orderFront:nil];
      } else {
        [mWindow makeKeyAndOrderFront:nil];
      }
      NS_OBJC_END_TRY_IGNORE_BLOCK;
    }
    SetSupportsNativeFullscreen(savedValueForSupportsNativeFullscreen);
  } else {
    // roll up any popups if a top-level window is going away
    if (mWindowType == WindowType::TopLevel ||
        mWindowType == WindowType::Dialog) {
      RollUpPopups();
    }

    // If the window is a popup window with a parent window we need to
    // unhook it here before ordering it out. When you order out the child
    // of a window it hides the parent window.
    if (mWindowType == WindowType::Popup && nativeParentWindow) {
      [nativeParentWindow removeChildWindow:mWindow];
    }

    [mWindow orderOut:nil];
    // If our popup window is a non-native context menu, tell the OS (and
    // other programs) that a menu has closed.
    if ([mWindow isKindOfClass:[PopupWindow class]] &&
        [(PopupWindow*)mWindow isContextMenu]) {
      [NSDistributedNotificationCenter.defaultCenter
          postNotificationName:
              @"com.apple.HIToolbox.endMenuTrackingNotification"
                        object:@"org.mozilla.gecko.PopupWindow"];
    }
  }

  [mWindow setBeingShown:NO];

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

// Work around a problem where with multiple displays and multiple spaces
// enabled, where the browser is assigned to a single display or space, popup
// windows that are reshown after being hidden with [NSWindow orderOut] show on
// the assigned space even when opened from another display. Apply the
// workaround whenever more than one display is enabled.
bool nsCocoaWindow::NeedsRecreateToReshow() {
  // Limit the workaround to popup windows because only they need to override
  // the "Assign To" setting. i.e., to display where the parent window is.
  return mWindowType == WindowType::Popup && mWasShown &&
         NSScreen.screens.count > 1;
}

WindowRenderer* nsCocoaWindow::GetWindowRenderer() {
  if (mPopupContentView) {
    return mPopupContentView->GetWindowRenderer();
  }
  return nullptr;
}

TransparencyMode nsCocoaWindow::GetTransparencyMode() {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  return mWindow.isOpaque ? TransparencyMode::Opaque
                          : TransparencyMode::Transparent;

  NS_OBJC_END_TRY_BLOCK_RETURN(TransparencyMode::Opaque);
}

// This is called from nsMenuPopupFrame when making a popup transparent.
void nsCocoaWindow::SetTransparencyMode(TransparencyMode aMode) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow) {
    return;
  }

  BOOL isTransparent = aMode == TransparencyMode::Transparent;
  BOOL currentTransparency = !mWindow.isOpaque;
  if (isTransparent == currentTransparency) {
    return;
  }
  mWindow.opaque = !isTransparent;
  mWindow.backgroundColor =
      isTransparent ? NSColor.clearColor : NSColor.whiteColor;

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::Enable(bool aState) {}

bool nsCocoaWindow::IsEnabled() const { return true; }

void nsCocoaWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow || ![mWindow screen]) {
    return;
  }

  DesktopIntRect screenRect;

  int32_t width, height;

  NSRect frame = mWindow.frame;

  // zero size rects confuse the screen manager
  width = std::max<int32_t>(frame.size.width, 1);
  height = std::max<int32_t>(frame.size.height, 1);

  nsCOMPtr<nsIScreenManager> screenMgr =
      do_GetService("@mozilla.org/gfx/screenmanager;1");
  if (screenMgr) {
    nsCOMPtr<nsIScreen> screen;
    screenMgr->ScreenForRect(aPoint.x, aPoint.y, width, height,
                             getter_AddRefs(screen));

    if (screen) {
      screenRect = screen->GetRectDisplayPix();
    }
  }

  aPoint = ConstrainPositionToBounds(aPoint, {width, height}, screenRect);

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  // Popups can be smaller than (32, 32)
  NSRect rect = (mWindowType == WindowType::Popup)
                    ? NSZeroRect
                    : NSMakeRect(0.0, 0.0, 32, 32);
  rect = [mWindow frameRectForChildViewRect:rect];

  SizeConstraints c = aConstraints;

  if (c.mScale.scale == MOZ_WIDGET_INVALID_SCALE) {
    c.mScale.scale = BackingScaleFactor();
  }

  c.mMinSize.width = std::max(
      nsCocoaUtils::CocoaPointsToDevPixels(rect.size.width, c.mScale.scale),
      c.mMinSize.width);
  c.mMinSize.height = std::max(
      nsCocoaUtils::CocoaPointsToDevPixels(rect.size.height, c.mScale.scale),
      c.mMinSize.height);

  NSSize minSize = {
      nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.width, c.mScale.scale),
      nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.height, c.mScale.scale)};
  mWindow.minSize = minSize;

  c.mMaxSize.width = std::max(
      nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.width, c.mScale.scale),
      c.mMaxSize.width);
  c.mMaxSize.height = std::max(
      nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.height, c.mScale.scale),
      c.mMaxSize.height);

  NSSize maxSize = {
      c.mMaxSize.width == NS_MAXSIZE ? FLT_MAX
                                     : nsCocoaUtils::DevPixelsToCocoaPoints(
                                           c.mMaxSize.width, c.mScale.scale),
      c.mMaxSize.height == NS_MAXSIZE ? FLT_MAX
                                      : nsCocoaUtils::DevPixelsToCocoaPoints(
                                            c.mMaxSize.height, c.mScale.scale)};
  mWindow.maxSize = maxSize;
  nsBaseWidget::SetSizeConstraints(c);

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

// Coordinates are desktop pixels
void nsCocoaWindow::Move(double aX, double aY) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow) {
    return;
  }

  // The point we have is in Gecko coordinates (origin top-left). Convert
  // it to Cocoa ones (origin bottom-left).
  NSPoint coord = {
      static_cast<float>(aX),
      static_cast<float>(nsCocoaUtils::FlippedScreenY(NSToIntRound(aY)))};

  NSRect frame = mWindow.frame;
  if (frame.origin.x != coord.x ||
      frame.origin.y + frame.size.height != coord.y) {
    [mWindow setFrameTopLeftPoint:coord];
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::SetSizeMode(nsSizeMode aMode) {
  if (aMode == nsSizeMode_Normal) {
    QueueTransition(TransitionType::Windowed);
  } else if (aMode == nsSizeMode_Minimized) {
    QueueTransition(TransitionType::Miniaturize);
  } else if (aMode == nsSizeMode_Maximized) {
    QueueTransition(TransitionType::Zoom);
  } else if (aMode == nsSizeMode_Fullscreen) {
    MakeFullScreen(true);
  }
}

// The (work)space switching implementation below was inspired by Phoenix:
// https://github.com/kasper/phoenix/tree/d6c877f62b30a060dff119d8416b0934f76af534
// License: MIT.

// Runtime `CGSGetActiveSpace` library function feature detection.
typedef CGSSpaceID (*CGSGetActiveSpaceFunc)(CGSConnection cid);
static CGSGetActiveSpaceFunc GetCGSGetActiveSpaceFunc() {
  static CGSGetActiveSpaceFunc func = nullptr;
  static bool lookedUpFunc = false;
  if (!lookedUpFunc) {
    func = (CGSGetActiveSpaceFunc)dlsym(RTLD_DEFAULT, "CGSGetActiveSpace");
    lookedUpFunc = true;
  }
  return func;
}
// Runtime `CGSCopyManagedDisplaySpaces` library function feature detection.
typedef CFArrayRef (*CGSCopyManagedDisplaySpacesFunc)(CGSConnection cid);
static CGSCopyManagedDisplaySpacesFunc GetCGSCopyManagedDisplaySpacesFunc() {
  static CGSCopyManagedDisplaySpacesFunc func = nullptr;
  static bool lookedUpFunc = false;
  if (!lookedUpFunc) {
    func = (CGSCopyManagedDisplaySpacesFunc)dlsym(
        RTLD_DEFAULT, "CGSCopyManagedDisplaySpaces");
    lookedUpFunc = true;
  }
  return func;
}
// Runtime `CGSCopySpacesForWindows` library function feature detection.
typedef CFArrayRef (*CGSCopySpacesForWindowsFunc)(CGSConnection cid,
                                                  CGSSpaceMask mask,
                                                  CFArrayRef windowIDs);
static CGSCopySpacesForWindowsFunc GetCGSCopySpacesForWindowsFunc() {
  static CGSCopySpacesForWindowsFunc func = nullptr;
  static bool lookedUpFunc = false;
  if (!lookedUpFunc) {
    func = (CGSCopySpacesForWindowsFunc)dlsym(RTLD_DEFAULT,
                                              "CGSCopySpacesForWindows");
    lookedUpFunc = true;
  }
  return func;
}
// Runtime `CGSAddWindowsToSpaces` library function feature detection.
typedef void (*CGSAddWindowsToSpacesFunc)(CGSConnection cid,
                                          CFArrayRef windowIDs,
                                          CFArrayRef spaceIDs);
static CGSAddWindowsToSpacesFunc GetCGSAddWindowsToSpacesFunc() {
  static CGSAddWindowsToSpacesFunc func = nullptr;
  static bool lookedUpFunc = false;
  if (!lookedUpFunc) {
    func =
        (CGSAddWindowsToSpacesFunc)dlsym(RTLD_DEFAULT, "CGSAddWindowsToSpaces");
    lookedUpFunc = true;
  }
  return func;
}
// Runtime `CGSRemoveWindowsFromSpaces` library function feature detection.
typedef void (*CGSRemoveWindowsFromSpacesFunc)(CGSConnection cid,
                                               CFArrayRef windowIDs,
                                               CFArrayRef spaceIDs);
static CGSRemoveWindowsFromSpacesFunc GetCGSRemoveWindowsFromSpacesFunc() {
  static CGSRemoveWindowsFromSpacesFunc func = nullptr;
  static bool lookedUpFunc = false;
  if (!lookedUpFunc) {
    func = (CGSRemoveWindowsFromSpacesFunc)dlsym(RTLD_DEFAULT,
                                                 "CGSRemoveWindowsFromSpaces");
    lookedUpFunc = true;
  }
  return func;
}

void nsCocoaWindow::GetWorkspaceID(nsAString& workspaceID) {
  workspaceID.Truncate();
  int32_t sid = GetWorkspaceID();
  if (sid != 0) {
    workspaceID.AppendInt(sid);
  }
}

int32_t nsCocoaWindow::GetWorkspaceID() {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  // Mac OSX space IDs start at '1' (default space), so '0' means 'unknown',
  // effectively.
  CGSSpaceID sid = 0;

  CGSCopySpacesForWindowsFunc CopySpacesForWindows =
      GetCGSCopySpacesForWindowsFunc();
  if (!CopySpacesForWindows) {
    return sid;
  }

  CGSConnection cid = _CGSDefaultConnection();
  // Fetch all spaces that this window belongs to (in order).
  NSArray<NSNumber*>* spaceIDs = CFBridgingRelease(CopySpacesForWindows(
      cid, kCGSAllSpacesMask,
      (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ]));
  if ([spaceIDs count]) {
    // When spaces are found, return the first one.
    // We don't support a single window painted across multiple places for now.
    sid = [spaceIDs[0] integerValue];
  } else {
    // Fall back to the workspace that's currently active, which is '1' in the
    // common case.
    CGSGetActiveSpaceFunc GetActiveSpace = GetCGSGetActiveSpaceFunc();
    if (GetActiveSpace) {
      sid = GetActiveSpace(cid);
    }
  }

  return sid;

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::MoveToWorkspace(const nsAString& workspaceIDStr) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if ([NSScreen screensHaveSeparateSpaces] && [[NSScreen screens] count] > 1) {
    // We don't support moving to a workspace when the user has this option
    // enabled in Mission Control.
    return;
  }

  nsresult rv = NS_OK;
  int32_t workspaceID = workspaceIDStr.ToInteger(&rv);
  if (NS_FAILED(rv)) {
    return;
  }

  CGSConnection cid = _CGSDefaultConnection();
  int32_t currentSpace = GetWorkspaceID();
  // If an empty workspace ID is passed in (not valid on OSX), or when the
  // window is already on this workspace, we don't need to do anything.
  if (!workspaceID || workspaceID == currentSpace) {
    return;
  }

  CGSCopyManagedDisplaySpacesFunc CopyManagedDisplaySpaces =
      GetCGSCopyManagedDisplaySpacesFunc();
  CGSAddWindowsToSpacesFunc AddWindowsToSpaces = GetCGSAddWindowsToSpacesFunc();
  CGSRemoveWindowsFromSpacesFunc RemoveWindowsFromSpaces =
      GetCGSRemoveWindowsFromSpacesFunc();
  if (!CopyManagedDisplaySpaces || !AddWindowsToSpaces ||
      !RemoveWindowsFromSpaces) {
    return;
  }

  // Fetch an ordered list of all known spaces.
  NSArray* displaySpacesInfo = CFBridgingRelease(CopyManagedDisplaySpaces(cid));
  // When we found the space we're looking for, we can bail out of the loop
  // early, which this local variable is used for.
  BOOL found = false;
  for (NSDictionary<NSString*, id>* spacesInfo in displaySpacesInfo) {
    NSArray<NSNumber*>* sids =
        [spacesInfo[CGSSpacesKey] valueForKey:CGSSpaceIDKey];
    for (NSNumber* sid in sids) {
      // If we found our space in the list, we're good to go and can jump out of
      // this loop.
      if ((int)[sid integerValue] == workspaceID) {
        found = true;
        break;
      }
    }
    if (found) {
      break;
    }
  }

  // We were unable to find the space to correspond with the workspaceID as
  // requested, so let's bail out.
  if (!found) {
    return;
  }

  // First we add the window to the appropriate space.
  AddWindowsToSpaces(cid, (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
                     (__bridge CFArrayRef) @[ @(workspaceID) ]);
  // Then we remove the window from the active space.
  RemoveWindowsFromSpaces(cid,
                          (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
                          (__bridge CFArrayRef) @[ @(currentSpace) ]);

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::SuppressAnimation(bool aSuppress) {
  if ([mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
    mWindow.isAnimationSuppressed = aSuppress;
    mWindow.animationBehavior =
        aSuppress ? NSWindowAnimationBehaviorNone : mWindowAnimationBehavior;
  }
}

// This has to preserve the window's frame bounds.
// This method requires (as does the Windows impl.) that you call Resize shortly
// after calling HideWindowChrome. See bug 498835 for fixing this.
void nsCocoaWindow::HideWindowChrome(bool aShouldHide) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow || !mWindowMadeHere ||
      (mWindowType != WindowType::TopLevel &&
       mWindowType != WindowType::Dialog)) {
    return;
  }

  const BOOL isVisible = mWindow.isVisible;

  // Remove child windows.
  NSArray* childWindows = [mWindow childWindows];
  NSEnumerator* enumerator = [childWindows objectEnumerator];
  NSWindow* child = nil;
  while ((child = [enumerator nextObject])) {
    [mWindow removeChildWindow:child];
  }

  // Remove the views in the old window's content view.
  // The NSArray is autoreleased and retains its NSViews.
  NSArray<NSView*>* contentViewContents = [mWindow contentViewContents];
  for (NSView* view in contentViewContents) {
    [view removeFromSuperviewWithoutNeedingDisplay];
  }

  // Save state (like window title).
  NSMutableDictionary* state = [mWindow exportState];

  // Recreate the window with the right border style.
  NSRect frameRect = mWindow.frame;
  BOOL restorable = mWindow.restorable;
  DestroyNativeWindow();
  nsresult rv = CreateNativeWindow(
      frameRect, aShouldHide ? BorderStyle::None : mBorderStyle, true,
      restorable);
  NS_ENSURE_SUCCESS_VOID(rv);

  // Re-import state.
  [mWindow importState:state];

  // Add the old content view subviews to the new window's content view.
  for (NSView* view in contentViewContents) {
    [[mWindow contentView] addSubview:view];
  }

  // Reparent child windows.
  enumerator = [childWindows objectEnumerator];
  while ((child = [enumerator nextObject])) {
    [mWindow addChildWindow:child ordered:NSWindowAbove];
  }

  // Show the new window.
  if (isVisible) {
    bool wasAnimationSuppressed = mIsAnimationSuppressed;
    mIsAnimationSuppressed = true;
    Show(true);
    mIsAnimationSuppressed = wasAnimationSuppressed;
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

class FullscreenTransitionData : public nsISupports {
 public:
  NS_DECL_ISUPPORTS

  explicit FullscreenTransitionData(NSWindow* aWindow)
      : mTransitionWindow(aWindow) {}

  NSWindow* mTransitionWindow;

 private:
  virtual ~FullscreenTransitionData() { [mTransitionWindow close]; }
};

NS_IMPL_ISUPPORTS0(FullscreenTransitionData)

@interface FullscreenTransitionDelegate : NSObject <NSAnimationDelegate> {
 @public
  nsCocoaWindow* mWindow;
  nsIRunnable* mCallback;
}
@end

@implementation FullscreenTransitionDelegate
- (void)cleanupAndDispatch:(NSAnimation*)animation {
  [animation setDelegate:nil];
  [self autorelease];
  // The caller should have added ref for us.
  NS_DispatchToMainThread(already_AddRefed<nsIRunnable>(mCallback));
}

- (void)animationDidEnd:(NSAnimation*)animation {
  MOZ_ASSERT(animation == mWindow->FullscreenTransitionAnimation(),
             "Should be handling the only animation on the window");
  mWindow->ReleaseFullscreenTransitionAnimation();
  [self cleanupAndDispatch:animation];
}

- (void)animationDidStop:(NSAnimation*)animation {
  [self cleanupAndDispatch:animation];
}
@end

static bool AlwaysUsesNativeFullScreen() {
  return Preferences::GetBool("full-screen-api.macos-native-full-screen",
                              false);
}

/* virtual */ bool nsCocoaWindow::PrepareForFullscreenTransition(
    nsISupports** aData) {
  if (AlwaysUsesNativeFullScreen()) {
    return false;
  }

  // Our fullscreen transition creates a new window occluding this window.
  // That triggers an occlusion event which can cause DOM fullscreen requests
  // to fail due to the context not being focused at the time the focus check
  // is performed in the child process. Until the transition is cleaned up in
  // CleanupFullscreenTransition(), ignore occlusion events for this window.
  // If this method is changed to return false, the transition will not be
  // performed and mIgnoreOcclusionCount should not be incremented.
  MOZ_ASSERT(mIgnoreOcclusionCount >= 0);
  mIgnoreOcclusionCount++;

  nsCOMPtr<nsIScreen> widgetScreen = GetWidgetScreen();
  NSScreen* cocoaScreen = ScreenHelperCocoa::CocoaScreenForScreen(widgetScreen);

  NSWindow* win =
      [[NSWindow alloc] initWithContentRect:cocoaScreen.frame
                                  styleMask:NSWindowStyleMaskBorderless
                                    backing:NSBackingStoreBuffered
                                      defer:YES];
  [win setBackgroundColor:[NSColor blackColor]];
  [win setAlphaValue:0];
  [win setIgnoresMouseEvents:YES];
  [win setLevel:NSScreenSaverWindowLevel];
  [win makeKeyAndOrderFront:nil];

  auto data = new FullscreenTransitionData(win);
  *aData = data;
  NS_ADDREF(data);
  return true;
}

/* virtual */ void nsCocoaWindow::CleanupFullscreenTransition() {
  MOZ_ASSERT(mIgnoreOcclusionCount > 0);
  mIgnoreOcclusionCount--;
}

/* virtual */ void nsCocoaWindow::PerformFullscreenTransition(
    FullscreenTransitionStage aStage, uint16_t aDuration, nsISupports* aData,
    nsIRunnable* aCallback) {
  auto data = static_cast<FullscreenTransitionData*>(aData);
  FullscreenTransitionDelegate* delegate =
      [[FullscreenTransitionDelegate alloc] init];
  delegate->mWindow = this;
  // Storing already_AddRefed directly could cause static checking fail.
  delegate->mCallback = nsCOMPtr<nsIRunnable>(aCallback).forget().take();

  if (mFullscreenTransitionAnimation) {
    [mFullscreenTransitionAnimation stopAnimation];
    ReleaseFullscreenTransitionAnimation();
  }

  NSDictionary* dict = @{
    NSViewAnimationTargetKey : data->mTransitionWindow,
    NSViewAnimationEffectKey : aStage == eBeforeFullscreenToggle
        ? NSViewAnimationFadeInEffect
        : NSViewAnimationFadeOutEffect
  };
  mFullscreenTransitionAnimation =
      [[NSViewAnimation alloc] initWithViewAnimations:@[ dict ]];
  [mFullscreenTransitionAnimation setDelegate:delegate];
  [mFullscreenTransitionAnimation setDuration:aDuration / 1000.0];
  [mFullscreenTransitionAnimation startAnimation];
}

void nsCocoaWindow::CocoaWindowWillEnterFullscreen(bool aFullscreen) {
  MOZ_ASSERT(mUpdateFullscreenOnResize.isNothing());

  mHasStartedNativeFullscreen = true;

  // Ensure that we update our fullscreen state as early as possible, when the
  // resize happens.
  mUpdateFullscreenOnResize =
      Some(aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed);
}

void nsCocoaWindow::CocoaWindowDidEnterFullscreen(bool aFullscreen) {
  EndOurNativeTransition();
  mHasStartedNativeFullscreen = false;
  DispatchOcclusionEvent();

  // Check if aFullscreen matches our expected fullscreen state. It might not if
  // there was a failure somewhere along the way, in which case we'll recover
  // from that.
  bool receivedExpectedFullscreen = false;
  if (mUpdateFullscreenOnResize.isSome()) {
    bool expectingFullscreen =
        (*mUpdateFullscreenOnResize == TransitionType::Fullscreen);
    receivedExpectedFullscreen = (expectingFullscreen == aFullscreen);
  } else {
    receivedExpectedFullscreen = (mInFullScreenMode == aFullscreen);
  }

  TransitionType transition =
      aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed;
  if (receivedExpectedFullscreen) {
    // Everything is as expected. Update our state if needed.
    HandleUpdateFullscreenOnResize();
  } else {
    // We weren't expecting this fullscreen state. Update our fullscreen state
    // to the new reality.
    UpdateFullscreenState(aFullscreen, true);

    // If we have a current transition, switch it to match what we just did.
    if (mTransitionCurrent.isSome()) {
      mTransitionCurrent = Some(transition);
    }
  }

  // Whether we expected this transition or not, we're ready to finish it.
  FinishCurrentTransitionIfMatching(transition);
}

void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) {
  bool wasInFullscreen = mInFullScreenMode;
  mInFullScreenMode = aFullScreen;
  if (aNativeMode || mInNativeFullScreenMode) {
    mInNativeFullScreenMode = aFullScreen;
  }

  if (aFullScreen == wasInFullscreen) {
    return;
  }

  DispatchSizeModeEvent();

  // Notify the mainChildView with our new fullscreen state.
  nsChildView* mainChildView =
      static_cast<nsChildView*>([[mWindow mainChildView] widget]);
  if (mainChildView) {
    mainChildView->UpdateFullscreen(aFullScreen);
  }
}

nsresult nsCocoaWindow::MakeFullScreen(bool aFullScreen) {
  return DoMakeFullScreen(aFullScreen, AlwaysUsesNativeFullScreen());
}

nsresult nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen) {
  return DoMakeFullScreen(aFullScreen, true);
}

nsresult nsCocoaWindow::DoMakeFullScreen(bool aFullScreen,
                                         bool aUseSystemTransition) {
  if (!mWindow) {
    return NS_OK;
  }

  // Figure out what type of transition is being requested.
  TransitionType transition = TransitionType::Windowed;
  if (aFullScreen) {
    // Decide whether to use fullscreen or emulated fullscreen.
    transition =
        (aUseSystemTransition && (mWindow.collectionBehavior &
                                  NSWindowCollectionBehaviorFullScreenPrimary))
            ? TransitionType::Fullscreen
            : TransitionType::EmulatedFullscreen;
  }

  QueueTransition(transition);
  return NS_OK;
}

void nsCocoaWindow::QueueTransition(const TransitionType& aTransition) {
  mTransitionsPending.push(aTransition);
  ProcessTransitions();
}

void nsCocoaWindow::ProcessTransitions() {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK

  if (mInProcessTransitions) {
    return;
  }

  mInProcessTransitions = true;

  if (mProcessTransitionsPending) {
    mProcessTransitionsPending->Cancel();
    mProcessTransitionsPending = nullptr;
  }

  // Start a loop that will continue as long as we have transitions to process
  // and we aren't waiting on an asynchronous transition to complete. Any
  // transition that starts something async will `continue` this loop to exit.
  while (!mTransitionsPending.empty() && !IsInTransition()) {
    TransitionType nextTransition = mTransitionsPending.front();

    // We have to check for some incompatible transition states, and if we find
    // one, instead perform an alternative transition and leave the queue
    // untouched. If we add one of these transitions, we set
    // mIsTransitionCurrentAdded because we don't want to confuse listeners who
    // are expecting to receive exactly one event when the requested transition
    // has completed.
    switch (nextTransition) {
      case TransitionType::Fullscreen:
      case TransitionType::EmulatedFullscreen:
      case TransitionType::Windowed:
      case TransitionType::Zoom:
        // These can't handle miniaturized windows, so deminiaturize first.
        if (mWindow.miniaturized) {
          mTransitionCurrent = Some(TransitionType::Deminiaturize);
          mIsTransitionCurrentAdded = true;
        }
        break;
      case TransitionType::Miniaturize:
        // This can't handle fullscreen, so go to windowed first.
        if (mInFullScreenMode) {
          mTransitionCurrent = Some(TransitionType::Windowed);
          mIsTransitionCurrentAdded = true;
        }
        break;
      default:
        break;
    }

    // If mTransitionCurrent is still empty, then we use the nextTransition and
    // pop the queue.
    if (mTransitionCurrent.isNothing()) {
      mTransitionCurrent = Some(nextTransition);
      mTransitionsPending.pop();
    }

    switch (*mTransitionCurrent) {
      case TransitionType::Fullscreen: {
        if (!mInFullScreenMode) {
          // Run a local run loop until it is safe to start a native fullscreen
          // transition.
          NSRunLoop* localRunLoop = [NSRunLoop currentRunLoop];
          while (mWindow && !CanStartNativeTransition() &&
                 [localRunLoop runMode:NSDefaultRunLoopMode
                            beforeDate:[NSDate distantFuture]]) {
            // This loop continues to process events until
            // CanStartNativeTransition() returns true or our native
            // window has been destroyed.
          }

          // This triggers an async animation, so continue.
          [mWindow toggleFullScreen:nil];
          continue;
        }
        break;
      }

      case TransitionType::EmulatedFullscreen: {
        if (!mInFullScreenMode) {
          NSDisableScreenUpdates();
          mSuppressSizeModeEvents = true;
          // The order here matters. When we exit full screen mode, we need to
          // show the Dock first, otherwise the newly-created window won't have
          // its minimize button enabled. See bug 526282.
          nsCocoaUtils::HideOSChromeOnScreen(true);
          nsBaseWidget::InfallibleMakeFullScreen(true);
          mSuppressSizeModeEvents = false;
          NSEnableScreenUpdates();
          UpdateFullscreenState(true, false);
        }
        break;
      }

      case TransitionType::Windowed: {
        if (mInFullScreenMode) {
          if (mInNativeFullScreenMode) {
            // Run a local run loop until it is safe to start a native
            // fullscreen transition.
            NSRunLoop* localRunLoop = [NSRunLoop currentRunLoop];
            while (mWindow && !CanStartNativeTransition() &&
                   [localRunLoop runMode:NSDefaultRunLoopMode
                              beforeDate:[NSDate distantFuture]]) {
              // This loop continues to process events until
              // CanStartNativeTransition() returns true or our native
              // window has been destroyed.
            }

            // This triggers an async animation, so continue.
            [mWindow toggleFullScreen:nil];
            continue;
          } else {
            NSDisableScreenUpdates();
            mSuppressSizeModeEvents = true;
            // The order here matters. When we exit full screen mode, we need to
            // show the Dock first, otherwise the newly-created window won't
            // have its minimize button enabled. See bug 526282.
            nsCocoaUtils::HideOSChromeOnScreen(false);
            nsBaseWidget::InfallibleMakeFullScreen(false);
            mSuppressSizeModeEvents = false;
            NSEnableScreenUpdates();
            UpdateFullscreenState(false, false);
          }
        } else if (mWindow.zoomed) {
          [mWindow zoom:nil];

          // Check if we're still zoomed. If we are, we need to do *something*
          // to make the window smaller than the zoom size so Cocoa will treat
          // us as being out of the zoomed state. Otherwise, we could stay
          // zoomed and never be able to be "normal" from calls to SetSizeMode.
          if (mWindow.zoomed) {
            NSRect maximumFrame = mWindow.frame;
            const CGFloat INSET_OUT_OF_ZOOM = 20.0f;
            [mWindow setFrame:NSInsetRect(maximumFrame, INSET_OUT_OF_ZOOM,
                                          INSET_OUT_OF_ZOOM)
                      display:YES];
            MOZ_ASSERT(
                !mWindow.zoomed,
                "We should be able to unzoom by shrinking the frame a bit.");
          }
        }
        break;
      }

      case TransitionType::Miniaturize:
        if (!mWindow.miniaturized) {
          // This triggers an async animation, so continue.
          [mWindow miniaturize:nil];
          continue;
        }
        break;

      case TransitionType::Deminiaturize:
        if (mWindow.miniaturized) {
          // This triggers an async animation, so continue.
          [mWindow deminiaturize:nil];
          continue;
        }
        break;

      case TransitionType::Zoom:
        if (!mWindow.zoomed) {
          [mWindow zoom:nil];
        }
        break;

      default:
        break;
    }

    mTransitionCurrent.reset();
    mIsTransitionCurrentAdded = false;
  }

  mInProcessTransitions = false;

  // When we finish processing transitions, dispatch a size mode event to cover
  // the cases where an inserted transition suppressed one, and the original
  // transition never sent one because it detected it was at the desired state
  // when it ran. If we've already sent a size mode event, then this will be a
  // no-op.
  if (!IsInTransition()) {
    DispatchSizeModeEvent();
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

void nsCocoaWindow::CancelAllTransitions() {
  // Clear our current and pending transitions. This simplifies our
  // reasoning about what happens next, and ensures that whatever is
  // currently happening won't trigger another call to
  // ProcessTransitions().
  mTransitionCurrent.reset();
  mIsTransitionCurrentAdded = false;
  if (mProcessTransitionsPending) {
    mProcessTransitionsPending->Cancel();
    mProcessTransitionsPending = nullptr;
  }
  std::queue<TransitionType>().swap(mTransitionsPending);
}

void nsCocoaWindow::FinishCurrentTransitionIfMatching(
    const TransitionType& aTransition) {
  // We've just finished some transition activity, and we're not sure whether it
  // was triggered programmatically, or by the user. If it matches our current
  // transition, then assume it was triggered programmatically and we can clean
  // up that transition and start processing transitions again.

  // Whether programmatic or user-initiated, we send out a size mode event.
  DispatchSizeModeEvent();

  if (mTransitionCurrent.isSome() && (*mTransitionCurrent == aTransition)) {
    // This matches our current transition, so do the safe parts of transition
    // cleanup.
    mTransitionCurrent.reset();
    mIsTransitionCurrentAdded = false;

    // Since this function is called from nsWindowDelegate transition callbacks,
    // we want to make sure those callbacks are all the way done before we
    // continue processing more transitions. To accomplish this, we dispatch
    // ProcessTransitions on the next event loop. Doing this will ensure that
    // any async native transition methods we call (like toggleFullScreen) will
    // succeed.
    if (!mTransitionsPending.empty() && !mProcessTransitionsPending) {
      mProcessTransitionsPending = NS_NewCancelableRunnableFunction(
          "ProcessTransitionsPending",
          [self = RefPtr{this}] { self->ProcessTransitions(); });
      NS_DispatchToCurrentThread(mProcessTransitionsPending);
    }
  }
}

bool nsCocoaWindow::HandleUpdateFullscreenOnResize() {
  if (mUpdateFullscreenOnResize.isNothing()) {
    return false;
  }

  bool toFullscreen =
      (*mUpdateFullscreenOnResize == TransitionType::Fullscreen);
  mUpdateFullscreenOnResize.reset();
  UpdateFullscreenState(toFullscreen, true);

  return true;
}

/* static */ nsCocoaWindow* nsCocoaWindow::sWindowInNativeTransition(nullptr);

bool nsCocoaWindow::CanStartNativeTransition() {
  if (sWindowInNativeTransition == nullptr) {
    // Claim it and return true, indicating that the caller has permission to
    // start the native fullscreen transition.
    sWindowInNativeTransition = this;
    return true;
  }
  return false;
}

void nsCocoaWindow::EndOurNativeTransition() {
  if (sWindowInNativeTransition == this) {
    sWindowInNativeTransition = nullptr;
  }
}

// Coordinates are desktop pixels
void nsCocoaWindow::DoResize(double aX, double aY, double aWidth,
                             double aHeight, bool aRepaint,
                             bool aConstrainToCurrentScreen) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (!mWindow || mInResize) {
    return;
  }

  // We are able to resize a window outside of any aspect ratio contraints
  // applied to it, but in order to "update" the aspect ratio contraint to the
  // new window dimensions, we must re-lock the aspect ratio.
  auto relockAspectRatio = MakeScopeExit([&]() {
    if (mAspectRatioLocked) {
      LockAspectRatio(true);
    }
  });

  AutoRestore<bool> reentrantResizeGuard(mInResize);
  mInResize = true;

  CGFloat scale = mSizeConstraints.mScale.scale;
  if (scale == MOZ_WIDGET_INVALID_SCALE) {
    scale = BackingScaleFactor();
  }

  // mSizeConstraints is in device pixels.
  int32_t width = NSToIntRound(aWidth * scale);
  int32_t height = NSToIntRound(aHeight * scale);

  width = std::max(mSizeConstraints.mMinSize.width,
                   std::min(mSizeConstraints.mMaxSize.width, width));
  height = std::max(mSizeConstraints.mMinSize.height,
                    std::min(mSizeConstraints.mMaxSize.height, height));

  DesktopIntRect newBounds(NSToIntRound(aX), NSToIntRound(aY),
                           NSToIntRound(width / scale),
                           NSToIntRound(height / scale));

  // convert requested bounds into Cocoa coordinate system
  NSRect newFrame = nsCocoaUtils::GeckoRectToCocoaRect(newBounds);

  NSRect frame = mWindow.frame;
  BOOL isMoving = newFrame.origin.x != frame.origin.x ||
                  newFrame.origin.y != frame.origin.y;
  BOOL isResizing = newFrame.size.width != frame.size.width ||
                    newFrame.size.height != frame.size.height;

  if (!isMoving && !isResizing) {
    return;
  }

  // We ignore aRepaint -- we have to call display:YES, otherwise the
  // title bar doesn't immediately get repainted and is displayed in
  // the wrong place, leading to a visual jump.
  [mWindow setFrame:newFrame display:YES];

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

// Coordinates are desktop pixels
void nsCocoaWindow::Resize(double aX, double aY, double aWidth, double aHeight,
                           bool aRepaint) {
  DoResize(aX, aY, aWidth, aHeight, aRepaint, false);
}

// Coordinates are desktop pixels
void nsCocoaWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
  double invScale = 1.0 / BackingScaleFactor();
  DoResize(mBounds.x * invScale, mBounds.y * invScale, aWidth, aHeight,
           aRepaint, true);
}

// Return the area that the Gecko ChildView in our window should cover, as an
// NSRect in screen coordinates (with 0,0 being the bottom left corner of the
// primary screen).
NSRect nsCocoaWindow::GetClientCocoaRect() {
  if (!mWindow) {
    return NSZeroRect;
  }

  return [mWindow childViewRectForFrameRect:mWindow.frame];
}

LayoutDeviceIntRect nsCocoaWindow::GetClientBounds() {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  CGFloat scaleFactor = BackingScaleFactor();
  return nsCocoaUtils::CocoaRectToGeckoRectDevPix(GetClientCocoaRect(),
                                                  scaleFactor);

  NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0));
}

void nsCocoaWindow::UpdateBounds() {
  NSRect frame = NSZeroRect;
  if (mWindow) {
    frame = mWindow.frame;
  }
  mBounds =
      nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor());

  if (mPopupContentView) {
    mPopupContentView->UpdateBoundsFromView();
  }
}

LayoutDeviceIntRect nsCocoaWindow::GetScreenBounds() {
--> --------------------

--> maximum size reached

--> --------------------

[ Dauer der Verarbeitung: 0.25 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge