1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988
|
// 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"
#include "ui/accessibility/platform/ax_platform_node.h"
#import "ui/base/cocoa/user_interface_item_command_handler.h"
#import "ui/base/cocoa/window_size_constants.h"
#include "ui/gfx/native_widget_types.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;
- (BOOL)_isNonactivatingPanel;
@end
struct NSEdgeAndCornerThicknesses {
double top, topLeft, left, bottomLeft, bottom, bottomRight, right, topRight;
};
@interface NSWindow (NSWindowResizing)
+ (void)_getExteriorResizeEdgeThicknesses:
(NSEdgeAndCornerThicknesses*)outThicknesses
forStyleMask:(NSWindowStyleMask)styleMask;
@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 _activationIndependence;
BOOL _isTooltip;
BOOL _isHeadless;
BOOL _isShufflingForOrdering;
BOOL _miniaturizationInProgress;
}
@synthesize bridgedNativeWidgetId = _bridgedNativeWidgetId;
@synthesize bridge = _bridge;
@synthesize isTooltip = _isTooltip;
@synthesize isHeadless = _isHeadless;
@synthesize isShufflingForOrdering = _isShufflingForOrdering;
@synthesize preventKeyWindow = _preventKeyWindow;
@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;
}
// Handle ordering groups for AppKit native windows. For instance, the
// `TUINSWindow` is added and removed by AppKit when caps lock is active.
// See https://crbug.com/369970893 for more details.
if (![childWin isKindOfClass:[NativeWidgetMacNSWindow class]]) {
[self maybeRemoveTreeFromOrderingGroups];
}
[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;
}
// This override, if it returns YES, allows the window to take input events
// without activating the owning app. This is functionally equivalent to having
// the window style NSWindowStyleMaskNonactivatingPanel set; see the
// documentation for that constant for more details.
//
// The NSWindowStyleMaskNonactivatingPanel constant is only valid for NSPanels,
// not NSWindows, so the window style cannot be directly set. In addition, even
// if it were valid to set that style for windows, setting the window style
// recalculates and re-caches a bunch of stuff, so a surgical override is the
// cleanest approach.
- (BOOL)_isNonactivatingPanel {
if (_activationIndependence) {
return YES;
}
return [super _isNonactivatingPanel];
}
+ (void)_getExteriorResizeEdgeThicknesses:
(NSEdgeAndCornerThicknesses*)outThicknesses
forStyleMask:(NSWindowStyleMask)styleMask {
// Ensure non-titled resizable windows have a reasonable exterior resize area.
// By default, they might have none, making resizing difficult.
// Override to titled window's resize edge thickness (4px on macOS 15).
if (styleMask & NSWindowStyleMaskResizable) {
return [super
_getExteriorResizeEdgeThicknesses:outThicknesses
forStyleMask:styleMask | NSWindowStyleMaskTitled];
}
return [super _getExteriorResizeEdgeThicknesses:outThicknesses
forStyleMask:styleMask];
}
// 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];
}
- (void)setActivationIndependence:(BOOL)independence {
self.canHide = !independence;
_activationIndependence = independence;
}
- (bool)activationIndependence {
return _activationIndependence;
}
// 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;
// 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:YES];
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 ![self immersiveFullscreen];
}
- (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).
- (NSString*)accessibilityDocument {
if (id<NSAccessibility> root = [self rootAccessibilityObject]) {
if (auto* cocoaNode = ui::AXPlatformNode::FromNativeViewAccessible(
gfx::NativeViewAccessible(root))) {
return [NSString stringWithUTF8String:cocoaNode->GetRootURL().c_str()];
}
}
return nil;
}
- (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
|