chromium/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"

#include "base/apple/foundation_util.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/debug/dump_without_crashing.h"
#include "base/debug/stack_trace.h"
#include "base/feature_list.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/crash/core/common/crash_key.h"
#import "components/remote_cocoa/app_shim/features.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_host_helper.h"
#import "components/remote_cocoa/app_shim/views_nswindow_delegate.h"
#import "components/remote_cocoa/app_shim/window_touch_bar_delegate.h"
#include "components/remote_cocoa/common/native_widget_ns_window_host.mojom.h"
#import "ui/base/cocoa/user_interface_item_command_handler.h"
#import "ui/base/cocoa/window_size_constants.h"

namespace {

bool AreWindowShadowsDisabled() {
  // When:
  // 1) Shadows are being generated by the window server
  // 2) The window with the shadow has a layer (all of Chrome's do)
  // 3) Software compositing is in use (it is in most test configs, which
  //    run in VMs)
  // 4) There are many windows in use at once (they are when running
  //    test in parallel)
  // The window server seems to crash with distressing frequency. To hopefully
  // mitigate that, disable window shadows when running on a bot.
  // For context on this see:
  //   https://crbug.com/899286
  //   https://crbug.com/828031
  //   https://crbug.com/515627, especially #63 and #67
  static bool is_headless = getenv("CHROME_HEADLESS") != nullptr;
  return is_headless;
}

// AppKit quirk: -[NSWindow orderWindow] does not handle reordering for children
// windows. Their order is fixed to the attachment order (the last attached
// window is on the top). Therefore, work around it by re-parenting in our
// desired order.
void OrderChildWindow(NSWindow* child_window,
                      NSWindow* other_window,
                      NSWindowOrderingMode ordering_mode) {
  NSWindow* parent = [child_window parentWindow];
  DCHECK(parent);

  // `ordered_children` sorts children windows back to front.
  NSArray<NSWindow*>* children = [[child_window parentWindow] childWindows];
  std::vector<std::pair<NSInteger, NSWindow*>> ordered_children;
  for (NSWindow* child in children) {
    ordered_children.emplace_back([child orderedIndex], child);
  }
  std::sort(ordered_children.begin(), ordered_children.end(), std::greater<>());

  // If `other_window` is nullptr, place `child_window` in front of (or behind)
  // all other children windows.
  if (other_window == nullptr) {
    other_window = ordering_mode == NSWindowAbove
                       ? ordered_children.back().second
                       : parent;
  }

  if (child_window == other_window) {
    return;
  }

  const bool relative_to_parent = parent == other_window;
  DCHECK(ordering_mode != NSWindowBelow || !relative_to_parent)
      << "Placing a child window behind its parent is not supported.";

  for (NSWindow* child in children) {
    [parent removeChildWindow:child];
  }

  // If `relative_to_parent` is true, `child_window` is the first child of its
  // parent.
  if (relative_to_parent) {
    [parent addChildWindow:child_window ordered:NSWindowAbove];
  }

  // Re-parent children windows in the desired order.
  for (auto [ordered_index, child] : ordered_children) {
    if (child != child_window && child != other_window) {
      [parent addChildWindow:child ordered:NSWindowAbove];
    } else if (child == other_window && !relative_to_parent) {
      if (ordering_mode == NSWindowAbove) {
        [parent addChildWindow:other_window ordered:NSWindowAbove];
        [parent addChildWindow:child_window ordered:NSWindowAbove];
      } else {
        [parent addChildWindow:child_window ordered:NSWindowAbove];
        [parent addChildWindow:other_window ordered:NSWindowAbove];
      }
    }
  }
}

}  // namespace

@interface NSNextStepFrame (Private)
- (instancetype)initWithFrame:(NSRect)frame
                    styleMask:(NSUInteger)styleMask
                        owner:(id)owner;
@end

@interface NSWindow (Private)
+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle;
- (BOOL)hasKeyAppearance;
- (long long)_resizeDirectionForMouseLocation:(CGPoint)location;
- (BOOL)_isConsideredOpenForPersistentState;
- (void)_zoomToScreenEdge:(NSUInteger)edge;
- (void)_removeFromGroups:(NSWindow*)window;
@end

// Private API as of at least macOS 13.
@interface NSWindow (NSWindow_Theme)
- (void)_regularMinimizeToDock;
@end

@interface NativeWidgetMacNSWindow () <NSKeyedArchiverDelegate>
- (ViewsNSWindowDelegate*)viewsNSWindowDelegate;
- (BOOL)hasViewsMenuActive;
- (id<NSAccessibility>)rootAccessibilityObject;

// Private API on NSWindow, determines whether the title is drawn on the title
// bar. The title is still visible in menus, Expose, etc.
- (BOOL)_isTitleHidden;
@end

// Use this category to implement mouseDown: on multiple frame view classes
// with different superclasses.
@interface NSView (CRFrameViewAdditions)
- (void)cr_mouseDownOnFrameView:(NSEvent*)event;
@end

@implementation NSView (CRFrameViewAdditions)
// If a mouseDown: falls through to the frame view, turn it into a window drag.
- (void)cr_mouseDownOnFrameView:(NSEvent*)event {
  if ([self.window _resizeDirectionForMouseLocation:event.locationInWindow] !=
      -1)
    return;
  [self.window performWindowDragWithEvent:event];
}
@end

@implementation NativeWidgetMacNSWindowTitledFrame
- (void)mouseDown:(NSEvent*)event {
  if (self.window.isMovable)
    [self cr_mouseDownOnFrameView:event];
  [super mouseDown:event];
}
- (BOOL)usesCustomDrawing {
  return NO;
}
// The base implementation just tests [self class] == [NSThemeFrame class].
- (BOOL)_shouldFlipTrafficLightsForRTL {
  return [[self window] windowTitlebarLayoutDirection] ==
         NSUserInterfaceLayoutDirectionRightToLeft;
}
@end

@implementation NativeWidgetMacNSWindowBorderlessFrame

- (void)mouseDown:(NSEvent*)event {
  [self cr_mouseDownOnFrameView:event];
  [super mouseDown:event];
}
- (BOOL)usesCustomDrawing {
  return NO;
}
@end

@implementation NativeWidgetMacNSWindow {
 @private
  CommandDispatcher* __strong _commandDispatcher;
  id<UserInterfaceItemCommandHandler> __strong _commandHandler;
  id<WindowTouchBarDelegate> __weak _touchBarDelegate;
  NSData* __strong _lastSavedRestorableState;
  uint64_t _bridgedNativeWidgetId;
  // This field is not a raw_ptr<> because it requires @property rewrite.
  RAW_PTR_EXCLUSION remote_cocoa::NativeWidgetNSWindowBridge* _bridge;
  BOOL _willUpdateRestorableState;
  BOOL _willSaveRestorableStateAfterDelay;
  BOOL _isEnforcingNeverMadeVisible;
  BOOL _preventKeyWindow;
  BOOL _isTooltip;
  BOOL _isHeadless;
  BOOL _isShufflingForOrdering;
  BOOL _miniaturizationInProgress;
}
@synthesize bridgedNativeWidgetId = _bridgedNativeWidgetId;
@synthesize bridge = _bridge;
@synthesize isTooltip = _isTooltip;
@synthesize isHeadless = _isHeadless;
@synthesize isShufflingForOrdering = _isShufflingForOrdering;
@synthesize childWindowAddedHandler = _childWindowAddedHandler;
@synthesize childWindowRemovedHandler = _childWindowRemovedHandler;
@synthesize commandDispatchParentOverride = _commandDispatchParentOverride;

- (instancetype)initWithContentRect:(NSRect)contentRect
                          styleMask:(NSUInteger)windowStyle
                            backing:(NSBackingStoreType)bufferingType
                              defer:(BOOL)deferCreation {
  DCHECK(NSEqualRects(contentRect, ui::kWindowSizeDeterminedLater));
  if ((self = [super initWithContentRect:ui::kWindowSizeDeterminedLater
                               styleMask:windowStyle
                                 backing:bufferingType
                                   defer:deferCreation])) {
    _commandDispatcher = [[CommandDispatcher alloc] initWithOwner:self];
    self.releasedWhenClosed = NO;
  }
  return self;
}

// This is called by the "Move Window to {Left/Right} Side of Screen"
// Window menu alternate items (must press Option to see).
// Without this, selecting these items will move child windows like
// bubbles and the find bar, but these should not be movable.
// Instead, let's push this up to the parent window which should be
// the browser.
- (void)_zoomToScreenEdge:(NSUInteger)edge {
  if (self.parentWindow) {
    [self.parentWindow _zoomToScreenEdge:edge];
  } else {
    [super _zoomToScreenEdge:edge];
  }
}

// This override helps diagnose lifetime issues in crash stacktraces by
// inserting a symbol on NativeWidgetMacNSWindow and should be kept even if it
// does nothing.
- (void)dealloc {
  if (_isEnforcingNeverMadeVisible) {
    [self removeObserver:self forKeyPath:@"visible"];
  }
  _willUpdateRestorableState = YES;
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
}

- (void)addChildWindow:(NSWindow*)childWin ordered:(NSWindowOrderingMode)place {
  // Attaching a window to be a child window resets the window level, so
  // restore the window level afterwards.
  NSInteger level = childWin.level;
  [super addChildWindow:childWin ordered:place];
  childWin.level = level;
  if (self.childWindowAddedHandler) {
    self.childWindowAddedHandler(childWin);
  }
}

- (void)removeChildWindow:(NSWindow*)childWin {
  if (self != childWin.parentWindow) {
    return;
  }
  [super removeChildWindow:childWin];
  if (self.childWindowRemovedHandler) {
    self.childWindowRemovedHandler(childWin);
  }
}

- (void)enforceNeverMadeVisible {
  if (_isEnforcingNeverMadeVisible)
    return;
  _isEnforcingNeverMadeVisible = YES;
  [self addObserver:self
         forKeyPath:@"visible"
            options:NSKeyValueObservingOptionNew
            context:nil];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  if ([keyPath isEqual:@"visible"]) {
    DCHECK(_isEnforcingNeverMadeVisible);
    DCHECK_EQ(object, self);
    DCHECK_EQ(context, nil);
    if ([change[NSKeyValueChangeNewKey] boolValue])
      base::debug::DumpWithoutCrashing();
  }
  [super observeValueForKeyPath:keyPath
                       ofObject:object
                         change:change
                        context:context];
}

// Public methods.

- (void)setHasShadow:(BOOL)flag {
  [super setHasShadow:flag && !AreWindowShadowsDisabled()];
}

- (void)setCommandDispatcherDelegate:(id<CommandDispatcherDelegate>)delegate {
  [_commandDispatcher setDelegate:delegate];
}

- (void)setWindowTouchBarDelegate:(id<WindowTouchBarDelegate>)delegate {
  _touchBarDelegate = delegate;
}

- (void)orderFrontKeepWindowKeyState {
  _miniaturizationInProgress = NO;

  if ([self isOnActiveSpace]) {
    [self orderWindow:NSWindowAbove relativeTo:0];
    return;
  }
  // The OS will activate the window if it causes a space switch.
  // Temporarily prevent the window from becoming the key window until after
  // the space change completes.
  _preventKeyWindow = ![self isKeyWindow];
  __block id observer = [NSWorkspace.sharedWorkspace.notificationCenter
      addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
                  object:[NSWorkspace sharedWorkspace]
                   queue:[NSOperationQueue mainQueue]
              usingBlock:^(NSNotification* notification) {
                self->_preventKeyWindow = NO;
                [NSWorkspace.sharedWorkspace.notificationCenter
                    removeObserver:observer];
              }];
  [self orderWindow:NSWindowAbove relativeTo:0];
}

- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen*)screen {
  // Headless windows should not be constrained within the physical screen.
  if (_isHeadless) {
    return frameRect;
  }

  return [super constrainFrameRect:frameRect toScreen:screen];
}

// Private methods.

- (ViewsNSWindowDelegate*)viewsNSWindowDelegate {
  return base::apple::ObjCCastStrict<ViewsNSWindowDelegate>([self delegate]);
}

- (BOOL)hasViewsMenuActive {
  bool hasMenuController = false;
  if (_bridge)
    _bridge->host()->GetHasMenuController(&hasMenuController);
  return hasMenuController;
}

- (id<NSAccessibility>)rootAccessibilityObject {
  id<NSAccessibility> obj =
      _bridge ? _bridge->host_helper()->GetNativeViewAccessible() : nil;
  // We should like to DCHECK that the object returned implements the
  // NSAccessibility protocol, but the NSAccessibilityRemoteUIElement interface
  // does not conform.
  // TODO(crbug.com/41448396): Create a sub-class that does.
  return obj;
}

- (NSAccessibilityRole)accessibilityRole {
  return _isTooltip ? NSAccessibilityHelpTagRole : [super accessibilityRole];
}

// NSWindow overrides.

+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
  if (windowStyle & NSWindowStyleMaskTitled) {
    if (Class customFrame = [NativeWidgetMacNSWindowTitledFrame class])
      return customFrame;
  } else if (Class customFrame =
                 [NativeWidgetMacNSWindowBorderlessFrame class]) {
    return customFrame;
  }
  return [super frameViewClassForStyleMask:windowStyle];
}

- (BOOL)_isTitleHidden {
  bool shouldShowWindowTitle = YES;
  if (_bridge)
    _bridge->host()->GetShouldShowWindowTitle(&shouldShowWindowTitle);
  return !shouldShowWindowTitle;
}

// The base implementation returns YES if the window's frame view is a custom
// class, which causes undesirable changes in behavior. AppKit NSWindow
// subclasses are known to override it and return NO.
- (BOOL)_usesCustomDrawing {
  return NO;
}

// Ignore [super canBecome{Key,Main}Window]. The default is NO for windows with
// NSWindowStyleMaskBorderless, which is not the desired behavior.
// Note these can be called via -[NSWindow close] while the widget is being torn
// down, so check for a delegate.
- (BOOL)canBecomeKeyWindow {
  if (_preventKeyWindow)
    return NO;
  bool canBecomeKey = NO;
  if (_bridge)
    _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey);
  return canBecomeKey;
}

- (BOOL)canBecomeMainWindow {
  if (!_bridge)
    return NO;

  // Dialogs and bubbles shouldn't take large shadows away from their parent.
  if (_bridge->parent())
    return NO;

  bool canBecomeKey = NO;
  if (_bridge)
    _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey);
  return canBecomeKey;
}

// Lets the traffic light buttons on the parent window keep their active state.
- (BOOL)hasKeyAppearance {
  // Note that this function is called off of the main thread. In such cases,
  // it is not safe to access the mojo interface or the ui::Widget, as they are
  // not reentrant.
  // https://crbug.com/941506.
  if (![NSThread isMainThread])
    return [super hasKeyAppearance];
  if (_bridge) {
    bool isAlwaysRenderWindowAsKey = NO;
    _bridge->host()->GetAlwaysRenderWindowAsKey(&isAlwaysRenderWindowAsKey);
    if (isAlwaysRenderWindowAsKey)
      return YES;
  }
  return [super hasKeyAppearance];
}

// Override sendEvent to intercept window drag events and allow key events to be
// forwarded to a toolkit-views menu while it is active, and while still
// allowing any native subview to retain firstResponder status.
- (void)sendEvent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::sendEvent", "WindowNum",
               [self windowNumber]);

  // Let CommandDispatcher check if this is a redispatched event.
  if ([_commandDispatcher preSendEvent:event]) {
    TRACE_EVENT_INSTANT0("browser", "StopSendEvent", TRACE_EVENT_SCOPE_THREAD);
    return;
  }

  NSEventType type = [event type];

  // Draggable regions only respond to left-click dragging, but the system will
  // still suppress right-clicks in a draggable region. Forwarding right-clicks
  // and ctrl+left-clicks allows the underlying views to respond to right-click
  // to potentially bring up a frame context menu.
  if (type == NSEventTypeRightMouseDown ||
      (type == NSEventTypeLeftMouseDown &&
       ([event modifierFlags] & NSEventModifierFlagControl))) {
    if ([[self contentView] hitTest:event.locationInWindow] == nil) {
      [[self contentView] rightMouseDown:event];
      return;
    }
  } else if (type == NSEventTypeRightMouseUp) {
    if ([[self contentView] hitTest:event.locationInWindow] == nil) {
      [[self contentView] rightMouseUp:event];
      return;
    }
  } else if ([self hasViewsMenuActive]) {
    // Send to the menu, after converting the event into an action message using
    // the content view.
    if (type == NSEventTypeKeyDown) {
      [[self contentView] keyDown:event];
      return;
    } else if (type == NSEventTypeKeyUp) {
      [[self contentView] keyUp:event];
      return;
    }
  }

  [super sendEvent:event];
}

- (void)orderWindowByShuffling:(NSWindowOrderingMode)orderingMode
                    relativeTo:(NSInteger)otherWindowNumber {
  NativeWidgetMacNSWindow* parent =
      static_cast<NativeWidgetMacNSWindow*>([self parentWindow]);

  // This is not a child window. No need to patch.
  if (!parent) {
    [self orderWindow:orderingMode relativeTo:otherWindowNumber];
    return;
  }

  base::AutoReset<BOOL> shuffling(&_isShufflingForOrdering, YES);

  // `otherWindow` is nil if `otherWindowNumber` is 0. In this case, place
  // `self` at the top / bottom, depending on `orderingMode`.
  NSWindow* otherWindow = [NSApp windowWithWindowNumber:otherWindowNumber];
  if (otherWindow == nullptr || parent == [otherWindow parentWindow] ||
      parent == otherWindow) {
    OrderChildWindow(self, otherWindow, orderingMode);
  }

  [[self viewsNSWindowDelegate] onWindowOrderChanged:nil];
}

// Override window order functions to intercept other visibility changes. This
// is needed in addition to the -[NSWindow display] override because Cocoa
// hardly ever calls display, and reports -[NSWindow isVisible] incorrectly
// when ordering in a window for the first time.
// Note that this methods has no effect for children windows. Use
// -orderWindowByShuffling:relativeTo: instead.
- (void)orderWindow:(NSWindowOrderingMode)orderingMode
         relativeTo:(NSInteger)otherWindowNumber {
  [super orderWindow:orderingMode relativeTo:otherWindowNumber];
  [[self viewsNSWindowDelegate] onWindowOrderChanged:nil];
}

- (void)miniaturize:(id)sender {
  static const BOOL isMacOS13OrHigher = base::mac::MacOSMajorVersion() >= 13;
  // On macOS 13, the miniaturize operation appears to no longer be "atomic"
  // because of non-blocking roundtrip IPC with the Dock. We want to note here
  // that miniaturization is in progress. The process completes when we
  // reach -_regularMinimizeToDock:.
  _miniaturizationInProgress = isMacOS13OrHigher;

  [super miniaturize:sender];
}

- (void)_regularMinimizeToDock {
  // On macOS 13, a call to -miniaturize: kicks of an async round-trip IPC with
  // the Dock that ends up in this method. Unfortunately, it appears that if we
  // immediately follow a call to -miniaturize: with -makeKeyAndOrderFront:,
  // the AppKit doesn't cancel the in-flight round-trip IPC. As a result,
  // _regularMinimizeToDock gets called sometime after -makeKeyAndOrderFront:
  // and miniaturizes the window anyway. This is  a potential problem in
  // session restore where we might restart with a single browser window
  // sitting Dock. In that case, Session Restore creates the window,
  // miniaturizes to the dock, and then brings it back out. With this new macOS
  // 13 behavior (which seems like a bug), the browser window may not be
  // restored from the Dock.
  //
  // To get around this problem, if we arrive here and
  // _miniaturizationInProgress is NO, the miniaturization process was
  // cancelled by a call to -makeKeyAndOrderFront:. In that case, we don't want
  // to proceed with miniaturization.
  static const BOOL isMacOS13OrHigher = base::mac::MacOSMajorVersion() >= 13;
  if (isMacOS13OrHigher && !_miniaturizationInProgress) {
    return;
  }

  _miniaturizationInProgress = NO;
  [super _regularMinimizeToDock];
}

- (void)makeKeyAndOrderFront:(id)sender {
  _miniaturizationInProgress = NO;
  [super makeKeyAndOrderFront:sender];
}

- (void)orderOut:(id)sender {
  _miniaturizationInProgress = NO;
  [self maybeRemoveTreeFromOrderingGroups];
  [super orderOut:sender];
}

- (void)close {
  [self maybeRemoveTreeFromOrderingGroups];
  [super close];
}

// NSResponder implementation.

- (BOOL)performKeyEquivalent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::performKeyEquivalent", "WindowNum",
               [self windowNumber]);
  return [_commandDispatcher performKeyEquivalent:event];
}

- (void)cursorUpdate:(NSEvent*)theEvent {
  // The cursor provided by the delegate should only be applied within the
  // content area. This is because we rely on the contentView to track the
  // mouse cursor and forward cursorUpdate: messages up the responder chain.
  // The cursorUpdate: isn't handled in BridgedContentView because views-style
  // SetCapture() conflicts with the way tracking events are processed for
  // the view during a drag. Since the NSWindow is still in the responder chain
  // overriding cursorUpdate: here handles both cases.
  if (!NSPointInRect([theEvent locationInWindow], [[self contentView] frame])) {
    [super cursorUpdate:theEvent];
    return;
  }

  NSCursor* cursor = [[self viewsNSWindowDelegate] cursor];
  if (cursor)
    [cursor set];
  else
    [super cursorUpdate:theEvent];
}

- (NSTouchBar*)makeTouchBar {
  return _touchBarDelegate ? [_touchBarDelegate makeTouchBar] : nil;
}

// Called when the window is the delegate of the archiver passed to
// |-encodeRestorableStateWithCoder:|, below. It prevents the archiver from
// trying to encode the window or an NSView, say, to represent the first
// responder. When AppKit calls |-encodeRestorableStateWithCoder:|, it
// accomplishes the same thing by passing a custom coder.
- (id)archiver:(NSKeyedArchiver*)archiver willEncodeObject:(id)object {
  if (object == self)
    return nil;
  if ([object isKindOfClass:[NSView class]])
    return nil;
  return object;
}

- (void)saveRestorableState {
  if (!_bridge || ![self _isConsideredOpenForPersistentState]) {
    return;
  }

  // Certain conditions, such as in the Speedometer 3 benchmark, can trigger a
  // rapid succession of calls to saveRestorableState. If there's no pending
  // save of restorable state, save the state now. This ensures that the first
  // new state change gets saved immediately. Then, set up to save again 500ms
  // after the last request. This will coalesce a storm of restorable state
  // saves into the first and last requests. This might ultimately result in a
  // single save operation if the first and last states are identical.
  //
  // We take pains to save the first and last requests to ensure we get the
  // expected state save on browser close. For example, if a browser window
  // miniaturizes and then the browser quits within our 500ms delay, the
  // miniaturized state may not get saved. Even if the call to
  // -reallySaveRestorableState occurs in time, we might still be in trouble
  // because the save has to cross the remote cocoa boundary (and so is
  // dependent on a couple more turns of the run loop to get the save to take).
  if (!_willSaveRestorableStateAfterDelay) {
    [self reallySaveRestorableState];
    _willSaveRestorableStateAfterDelay = YES;
  }

  [NSObject cancelPreviousPerformRequestsWithTarget:self
                                           selector:@selector
                                           (reallySaveRestorableState)
                                             object:nil];
  [self performSelector:@selector(reallySaveRestorableState)
             withObject:nil
             afterDelay:0.5];
}

- (void)reallySaveRestorableState {
  _willSaveRestorableStateAfterDelay = NO;

  if (!_bridge) {
    return;
  }

  _willUpdateRestorableState = NO;

  // On macOS 12+, create restorable state archives with secure encoding. See
  // the article at
  // https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/
  // for more details.
  NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc]
      initRequiringSecureCoding:base::mac::MacOSMajorVersion() >= 12];
  encoder.delegate = self;
  [self encodeRestorableStateWithCoder:encoder];
  [encoder finishEncoding];
  NSData* restorableState = encoder.encodedData;

  // Don't bother saving restorable state if it didn't actually change since
  // the last save. This avoids an extra IPC when nothing has changed.
  if ([restorableState isEqual:_lastSavedRestorableState]) {
    return;
  }
  _lastSavedRestorableState = restorableState;

  auto* bytes = static_cast<uint8_t const*>(restorableState.bytes);
  _bridge->host()->OnWindowStateRestorationDataChanged(
      std::vector<uint8_t>(bytes, bytes + restorableState.length));
}

// AppKit calls -invalidateRestorableState when a property of the window which
// affects its restorable state changes.
- (void)invalidateRestorableState {
  [super invalidateRestorableState];
  if ([self _isConsideredOpenForPersistentState]) {
    if (_willUpdateRestorableState)
      return;
    _willUpdateRestorableState = YES;
    [self performSelectorOnMainThread:@selector(saveRestorableState)
                           withObject:nil
                        waitUntilDone:NO
                                modes:@[ NSDefaultRunLoopMode ]];
  } else if (_willUpdateRestorableState) {
    _willUpdateRestorableState = NO;
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
  }
}

// On newer SDKs, _canMiniaturize respects NSWindowStyleMaskMiniaturizable in
// the window's styleMask. Views assumes that Widgets can always be minimized,
// regardless of their window style, so override that behavior here.
- (BOOL)_canMiniaturize {
  return YES;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
  // If this window or its parent does not handle commands, remove it from the
  // chain.
  bool isCommandDispatch =
      aSelector == @selector(commandDispatch:) ||
      aSelector == @selector(commandDispatchUsingKeyModifiers:);
  if (isCommandDispatch && _commandHandler == nil &&
      self.commandDispatchParent == nil) {
    return NO;
  }

  return [super respondsToSelector:aSelector];
}

// CommandDispatchingWindow implementation.

- (void)setCommandHandler:(id<UserInterfaceItemCommandHandler>)commandHandler {
  _commandHandler = commandHandler;
}

- (CommandDispatcher*)commandDispatcher {
  return _commandDispatcher;
}

- (BOOL)defaultPerformKeyEquivalent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT1("browser", "NSWindow::defaultPerformKeyEquivalent", "WindowNum",
               [self windowNumber]);
  return [super performKeyEquivalent:event];
}

- (BOOL)defaultValidateUserInterfaceItem:
    (id<NSValidatedUserInterfaceItem>)item {
  return [super validateUserInterfaceItem:item];
}

- (void)commandDispatch:(id)sender {
  [_commandDispatcher dispatch:sender forHandler:_commandHandler];
}

- (void)commandDispatchUsingKeyModifiers:(id)sender {
  [_commandDispatcher dispatchUsingKeyModifiers:sender
                                     forHandler:_commandHandler];
}

// NSWindow overrides (NSUserInterfaceItemValidations implementation)

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return [_commandDispatcher validateUserInterfaceItem:item
                                            forHandler:_commandHandler];
}

// NSWindow overrides (NSAccessibility informal protocol implementation).

- (id)accessibilityFocusedUIElement {
  if (![self delegate])
    return [super accessibilityFocusedUIElement];

  // The SDK documents this as "The deepest descendant of the accessibility
  // hierarchy that has the focus" and says "if a child element does not have
  // the focus, either return self or, if available, invoke the superclass's
  // implementation."
  // The behavior of NSWindow is usually to return null, except when the window
  // is first shown, when it returns self. But in the second case, we can
  // provide richer a11y information by reporting the views::RootView instead.
  // Additionally, if we don't do this, VoiceOver reads out the partial a11y
  // properties on the NSWindow and repeats them when focusing an item in the
  // RootView's a11y group. See http://crbug.com/748221.
  id superFocus = [super accessibilityFocusedUIElement];
  if (!_bridge || superFocus != self)
    return superFocus;

  return _bridge->host_helper()->GetNativeViewAccessible();
}

- (NSString*)accessibilityTitle {
  // Check when NSWindow is asked for its title to provide the title given by
  // the views::RootView (and WidgetDelegate::GetAccessibleWindowTitle()). For
  // all other attributes, use what NSWindow provides by default since diverging
  // from NSWindow's behavior can easily break VoiceOver integration.
  NSString* viewsValue = self.rootAccessibilityObject.accessibilityTitle;
  return viewsValue ? viewsValue : [super accessibilityTitle];
}

- (NSWindow<CommandDispatchingWindow>*)commandDispatchParent {
  if (_commandDispatchParentOverride) {
    return _commandDispatchParentOverride;
  }
  NSWindow* parent = self.parentWindow;
  if (parent && [parent hasKeyAppearance] &&
      [parent conformsToProtocol:@protocol(CommandDispatchingWindow)]) {
    return static_cast<NSWindow<CommandDispatchingWindow>*>(parent);
  }
  return nil;
}

// During window ordering AppKit rebuilds its internal ordering group for the
// window tree. A window tree in Chrome's case is the browser window and all of
// its descendants, which can include Chrome and AppKit created windows. It does
// this by removing and re-adding each window in the window tree from the
// ordering group. If a window is re-added to the group while in a non-active
// space, a space switch can occur. The space switch will only happen if the
// current window tree has existing windows that are still a part of the
// ordering group. When there are two levels in the window tree, each window
// will be removed from the group before windows are re-added to the group. If
// three or more window levels exist in the tree not all windows will be removed
// from the group before windows are re-added to the group, causing a space
// switch to occur. It seems this is an unintentional side effect of AppKit's
// recursive window tree group rebuilding.
//
// To work around this behavior, preemptively remove the window tree from
// ordering groups. This workaround should be considered low risk, while we are
// calling an undocumented NSWindow method, removing the window tree from
// ordering groups is ubiquitous throughout AppKit. Additionally, this
// preemptive removal is only called before an -orderOut: or -close. AppKit will
// be doing an ordering group rebuild during those calls. We are also taking
// care to only apply this workaround when necessary.
//
// TODO(http://crbug.com/1454606): Remove this workaround once FB13529873 is
// fixed in AppKit.
- (void)maybeRemoveTreeFromOrderingGroups {
  // This workaround only needed for macOS 13 and greater.
  if (@available(macOS 13.0, *)) {
  } else {
    return;
  }

  if (!base::FeatureList::IsEnabled(
          remote_cocoa::features::kImmersiveFullscreenSpaceSwitchMitigation)) {
    return;
  }

  // Only remove from groups if this window is not on the active space.
  if (self.isOnActiveSpace) {
    return;
  }

  // Only remove from groups if the browser is in immersive fullscreen.
  if (![self immersiveFullscreen]) {
    return;
  }

  // Since _removeFromGroups: is not documented it could go away in newer
  // versions of macOS. If the selector does not exist, DumpWithoutCrashing() so
  // we hear about the change.
  if (![NSWindow instancesRespondToSelector:@selector(_removeFromGroups:)]) {
    base::debug::DumpWithoutCrashing();
    return;
  }

  // Iterate instead of recurse. There are other NSWindow types in the tree
  // besides NativeWidgetMacNSWindow that would not implement our recursion.
  NSMutableArray* nextWindows = [NSMutableArray array];
  [nextWindows addObject:[self rootWindow]];
  while (nextWindows.count) {
    NSWindow* currentWindow = nextWindows.lastObject;
    [nextWindows removeLastObject];
    for (NSWindow* child in currentWindow.childWindows) {
      [nextWindows addObject:child];
      [currentWindow _removeFromGroups:child];
    }
  }
}

- (NSWindow*)rootWindow {
  NSWindow* root = self;
  while (root.parentWindow) {
    root = root.parentWindow;
  }
  return root;
}

- (BOOL)immersiveFullscreen {
  NativeWidgetMacNSWindow* rootWidgetWindow =
      base::apple::ObjCCast<NativeWidgetMacNSWindow>([self rootWindow]);
  if (rootWidgetWindow &&
      (rootWidgetWindow.styleMask & NSWindowStyleMaskFullScreen) &&
      rootWidgetWindow.bridge &&
      rootWidgetWindow.bridge->ImmersiveFullscreenEnabled()) {
    return YES;
  }
  return NO;
}

- (NSWindow*)preferredSheetParent {
  return [self immersiveFullscreen] ? [self rootWindow] : self;
}

#ifndef NDEBUG
- (NSString*)debugDescription {
  if (!self.title.length) {
    return [super debugDescription];
  }
  return [NSString
      stringWithFormat:@"%@ - %@", [super debugDescription], self.title];
}
#endif  // NDEBUG

@end