1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h" 6 7#include "base/mac/foundation_util.h" 8#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" 9#include "components/remote_cocoa/app_shim/native_widget_ns_window_host_helper.h" 10#import "components/remote_cocoa/app_shim/views_nswindow_delegate.h" 11#import "components/remote_cocoa/app_shim/window_touch_bar_delegate.h" 12#include "components/remote_cocoa/common/native_widget_ns_window_host.mojom.h" 13#import "ui/base/cocoa/user_interface_item_command_handler.h" 14#import "ui/base/cocoa/window_size_constants.h" 15 16@interface NSWindow (Private) 17+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle; 18- (BOOL)hasKeyAppearance; 19- (long long)_resizeDirectionForMouseLocation:(CGPoint)location; 20- (BOOL)_isConsideredOpenForPersistentState; 21 22// Available in later point releases of 10.10. On 10.11+, use the public 23// -performWindowDragWithEvent: instead. 24- (void)beginWindowDragWithEvent:(NSEvent*)event; 25@end 26 27@interface NativeWidgetMacNSWindow () <NSKeyedArchiverDelegate> 28- (ViewsNSWindowDelegate*)viewsNSWindowDelegate; 29- (BOOL)hasViewsMenuActive; 30- (id<NSAccessibility>)rootAccessibilityObject; 31 32// Private API on NSWindow, determines whether the title is drawn on the title 33// bar. The title is still visible in menus, Expose, etc. 34- (BOOL)_isTitleHidden; 35@end 36 37// Use this category to implement mouseDown: on multiple frame view classes 38// with different superclasses. 39@interface NSView (CRFrameViewAdditions) 40- (void)cr_mouseDownOnFrameView:(NSEvent*)event; 41@end 42 43@implementation NSView (CRFrameViewAdditions) 44// If a mouseDown: falls through to the frame view, turn it into a window drag. 45- (void)cr_mouseDownOnFrameView:(NSEvent*)event { 46 if ([self.window _resizeDirectionForMouseLocation:event.locationInWindow] != 47 -1) 48 return; 49 if (@available(macOS 10.11, *)) 50 [self.window performWindowDragWithEvent:event]; 51 else if ([self.window 52 respondsToSelector:@selector(beginWindowDragWithEvent:)]) 53 [self.window beginWindowDragWithEvent:event]; 54 else 55 NOTREACHED(); 56} 57@end 58 59@implementation NativeWidgetMacNSWindowTitledFrame 60- (void)mouseDown:(NSEvent*)event { 61 if (self.window.isMovable) 62 [self cr_mouseDownOnFrameView:event]; 63 [super mouseDown:event]; 64} 65- (BOOL)usesCustomDrawing { 66 return NO; 67} 68// The base implementation just tests [self class] == [NSThemeFrame class]. 69- (BOOL)_shouldFlipTrafficLightsForRTL API_AVAILABLE(macos(10.12)) { 70 return [[self window] windowTitlebarLayoutDirection] == 71 NSUserInterfaceLayoutDirectionRightToLeft; 72} 73@end 74 75@implementation NativeWidgetMacNSWindowBorderlessFrame 76- (void)mouseDown:(NSEvent*)event { 77 [self cr_mouseDownOnFrameView:event]; 78 [super mouseDown:event]; 79} 80- (BOOL)usesCustomDrawing { 81 return NO; 82} 83@end 84 85@implementation NativeWidgetMacNSWindow { 86 @private 87 base::scoped_nsobject<CommandDispatcher> _commandDispatcher; 88 base::scoped_nsprotocol<id<UserInterfaceItemCommandHandler>> _commandHandler; 89 id<WindowTouchBarDelegate> _touchBarDelegate; // Weak. 90 uint64_t _bridgedNativeWidgetId; 91 remote_cocoa::NativeWidgetNSWindowBridge* _bridge; 92 BOOL _willUpdateRestorableState; 93} 94@synthesize bridgedNativeWidgetId = _bridgedNativeWidgetId; 95@synthesize bridge = _bridge; 96 97- (instancetype)initWithContentRect:(NSRect)contentRect 98 styleMask:(NSUInteger)windowStyle 99 backing:(NSBackingStoreType)bufferingType 100 defer:(BOOL)deferCreation { 101 DCHECK(NSEqualRects(contentRect, ui::kWindowSizeDeterminedLater)); 102 if ((self = [super initWithContentRect:ui::kWindowSizeDeterminedLater 103 styleMask:windowStyle 104 backing:bufferingType 105 defer:deferCreation])) { 106 _commandDispatcher.reset([[CommandDispatcher alloc] initWithOwner:self]); 107 } 108 return self; 109} 110 111// This override helps diagnose lifetime issues in crash stacktraces by 112// inserting a symbol on NativeWidgetMacNSWindow and should be kept even if it 113// does nothing. 114- (void)dealloc { 115 _willUpdateRestorableState = YES; 116 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 117 [super dealloc]; 118} 119 120// Public methods. 121 122- (void)setCommandDispatcherDelegate:(id<CommandDispatcherDelegate>)delegate { 123 [_commandDispatcher setDelegate:delegate]; 124} 125 126- (void)sheetDidEnd:(NSWindow*)sheet 127 returnCode:(NSInteger)returnCode 128 contextInfo:(void*)contextInfo { 129 // Note NativeWidgetNSWindowBridge may have cleared [self delegate], in which 130 // case this will no-op. This indirection is necessary to handle AppKit 131 // invoking this selector via a posted task. See https://crbug.com/851376. 132 [[self viewsNSWindowDelegate] sheetDidEnd:sheet 133 returnCode:returnCode 134 contextInfo:contextInfo]; 135} 136 137- (void)setWindowTouchBarDelegate:(id<WindowTouchBarDelegate>)delegate { 138 _touchBarDelegate = delegate; 139} 140 141// Private methods. 142 143- (ViewsNSWindowDelegate*)viewsNSWindowDelegate { 144 return base::mac::ObjCCastStrict<ViewsNSWindowDelegate>([self delegate]); 145} 146 147- (BOOL)hasViewsMenuActive { 148 bool hasMenuController = false; 149 if (_bridge) 150 _bridge->host()->GetHasMenuController(&hasMenuController); 151 return hasMenuController; 152} 153 154- (id<NSAccessibility>)rootAccessibilityObject { 155 id<NSAccessibility> obj = 156 _bridge ? _bridge->host_helper()->GetNativeViewAccessible() : nil; 157 // We should like to DCHECK that the object returned implemements the 158 // NSAccessibility protocol, but the NSAccessibilityRemoteUIElement interface 159 // does not conform. 160 // TODO(https://crbug.com/944698): Create a sub-class that does. 161 return obj; 162} 163 164// NSWindow overrides. 165 166+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle { 167 if (windowStyle & NSWindowStyleMaskTitled) { 168 if (Class customFrame = [NativeWidgetMacNSWindowTitledFrame class]) 169 return customFrame; 170 } else if (Class customFrame = 171 [NativeWidgetMacNSWindowBorderlessFrame class]) { 172 return customFrame; 173 } 174 return [super frameViewClassForStyleMask:windowStyle]; 175} 176 177- (BOOL)_isTitleHidden { 178 bool shouldShowWindowTitle = YES; 179 if (_bridge) 180 _bridge->host()->GetShouldShowWindowTitle(&shouldShowWindowTitle); 181 return !shouldShowWindowTitle; 182} 183 184// The base implementation returns YES if the window's frame view is a custom 185// class, which causes undesirable changes in behavior. AppKit NSWindow 186// subclasses are known to override it and return NO. 187- (BOOL)_usesCustomDrawing { 188 return NO; 189} 190 191// Ignore [super canBecome{Key,Main}Window]. The default is NO for windows with 192// NSBorderlessWindowMask, which is not the desired behavior. 193// Note these can be called via -[NSWindow close] while the widget is being torn 194// down, so check for a delegate. 195- (BOOL)canBecomeKeyWindow { 196 bool canBecomeKey = NO; 197 if (_bridge) 198 _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey); 199 return canBecomeKey; 200} 201 202- (BOOL)canBecomeMainWindow { 203 if (!_bridge) 204 return NO; 205 206 // Dialogs and bubbles shouldn't take large shadows away from their parent. 207 if (_bridge->parent()) 208 return NO; 209 210 bool canBecomeKey = NO; 211 if (_bridge) 212 _bridge->host()->GetCanWindowBecomeKey(&canBecomeKey); 213 return canBecomeKey; 214} 215 216// Lets the traffic light buttons on the parent window keep their active state. 217- (BOOL)hasKeyAppearance { 218 // Note that this function is called off of the main thread. In such cases, 219 // it is not safe to access the mojo interface or the ui::Widget, as they are 220 // not reentrant. 221 // https://crbug.com/941506. 222 if (![NSThread isMainThread]) 223 return [super hasKeyAppearance]; 224 if (_bridge) { 225 bool isAlwaysRenderWindowAsKey = NO; 226 _bridge->host()->GetAlwaysRenderWindowAsKey(&isAlwaysRenderWindowAsKey); 227 if (isAlwaysRenderWindowAsKey) 228 return YES; 229 } 230 return [super hasKeyAppearance]; 231} 232 233// Override sendEvent to intercept window drag events and allow key events to be 234// forwarded to a toolkit-views menu while it is active, and while still 235// allowing any native subview to retain firstResponder status. 236- (void)sendEvent:(NSEvent*)event { 237 // Let CommandDispatcher check if this is a redispatched event. 238 if ([_commandDispatcher preSendEvent:event]) 239 return; 240 241 NSEventType type = [event type]; 242 243 // Draggable regions only respond to left-click dragging, but the system will 244 // still suppress right-clicks in a draggable region. Forwarding right-clicks 245 // allows the underlying views to respond to right-click to potentially bring 246 // up a frame context menu. 247 if (type == NSRightMouseDown) { 248 if ([[self contentView] hitTest:event.locationInWindow] == nil) { 249 [[self contentView] rightMouseDown:event]; 250 return; 251 } 252 } else if (type == NSRightMouseUp) { 253 if ([[self contentView] hitTest:event.locationInWindow] == nil) { 254 [[self contentView] rightMouseUp:event]; 255 return; 256 } 257 } else if ([self hasViewsMenuActive]) { 258 // Send to the menu, after converting the event into an action message using 259 // the content view. 260 if (type == NSKeyDown) { 261 [[self contentView] keyDown:event]; 262 return; 263 } else if (type == NSKeyUp) { 264 [[self contentView] keyUp:event]; 265 return; 266 } 267 } 268 269 [super sendEvent:event]; 270} 271 272// Override window order functions to intercept other visibility changes. This 273// is needed in addition to the -[NSWindow display] override because Cocoa 274// hardly ever calls display, and reports -[NSWindow isVisible] incorrectly 275// when ordering in a window for the first time. 276- (void)orderWindow:(NSWindowOrderingMode)orderingMode 277 relativeTo:(NSInteger)otherWindowNumber { 278 [super orderWindow:orderingMode relativeTo:otherWindowNumber]; 279 [[self viewsNSWindowDelegate] onWindowOrderChanged:nil]; 280} 281 282// NSResponder implementation. 283 284- (BOOL)performKeyEquivalent:(NSEvent*)event { 285 return [_commandDispatcher performKeyEquivalent:event]; 286} 287 288- (void)cursorUpdate:(NSEvent*)theEvent { 289 // The cursor provided by the delegate should only be applied within the 290 // content area. This is because we rely on the contentView to track the 291 // mouse cursor and forward cursorUpdate: messages up the responder chain. 292 // The cursorUpdate: isn't handled in BridgedContentView because views-style 293 // SetCapture() conflicts with the way tracking events are processed for 294 // the view during a drag. Since the NSWindow is still in the responder chain 295 // overriding cursorUpdate: here handles both cases. 296 if (!NSPointInRect([theEvent locationInWindow], [[self contentView] frame])) { 297 [super cursorUpdate:theEvent]; 298 return; 299 } 300 301 NSCursor* cursor = [[self viewsNSWindowDelegate] cursor]; 302 if (cursor) 303 [cursor set]; 304 else 305 [super cursorUpdate:theEvent]; 306} 307 308- (NSTouchBar*)makeTouchBar API_AVAILABLE(macos(10.12.2)) { 309 return _touchBarDelegate ? [_touchBarDelegate makeTouchBar] : nil; 310} 311 312// Called when the window is the delegate of the archiver passed to 313// |-encodeRestorableStateWithCoder:|, below. It prevents the archiver from 314// trying to encode the window or an NSView, say, to represent the first 315// responder. When AppKit calls |-encodeRestorableStateWithCoder:|, it 316// accomplishes the same thing by passing a custom coder. 317- (id)archiver:(NSKeyedArchiver*)archiver willEncodeObject:(id)object { 318 if (object == self) 319 return nil; 320 if ([object isKindOfClass:[NSView class]]) 321 return nil; 322 return object; 323} 324 325- (void)saveRestorableState { 326 if (!_bridge) 327 return; 328 if (![self _isConsideredOpenForPersistentState]) 329 return; 330 base::scoped_nsobject<NSMutableData> restorableStateData( 331 [[NSMutableData alloc] init]); 332 base::scoped_nsobject<NSKeyedArchiver> encoder([[NSKeyedArchiver alloc] 333 initForWritingWithMutableData:restorableStateData]); 334 encoder.get().delegate = self; 335 [self encodeRestorableStateWithCoder:encoder]; 336 [encoder finishEncoding]; 337 338 auto* bytes = static_cast<uint8_t const*>(restorableStateData.get().bytes); 339 _bridge->host()->OnWindowStateRestorationDataChanged( 340 std::vector<uint8_t>(bytes, bytes + restorableStateData.get().length)); 341 _willUpdateRestorableState = NO; 342} 343 344// AppKit calls -invalidateRestorableState when a property of the window which 345// affects its restorable state changes. 346- (void)invalidateRestorableState { 347 [super invalidateRestorableState]; 348 if ([self _isConsideredOpenForPersistentState]) { 349 if (_willUpdateRestorableState) 350 return; 351 _willUpdateRestorableState = YES; 352 [self performSelectorOnMainThread:@selector(saveRestorableState) 353 withObject:nil 354 waitUntilDone:NO 355 modes:@[ NSDefaultRunLoopMode ]]; 356 } else if (_willUpdateRestorableState) { 357 _willUpdateRestorableState = NO; 358 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 359 } 360} 361 362// On newer SDKs, _canMiniaturize respects NSMiniaturizableWindowMask in the 363// window's styleMask. Views assumes that Widgets can always be minimized, 364// regardless of their window style, so override that behavior here. 365- (BOOL)_canMiniaturize { 366 return YES; 367} 368 369// CommandDispatchingWindow implementation. 370 371- (void)setCommandHandler:(id<UserInterfaceItemCommandHandler>)commandHandler { 372 _commandHandler.reset([commandHandler retain]); 373} 374 375- (CommandDispatcher*)commandDispatcher { 376 return _commandDispatcher.get(); 377} 378 379- (BOOL)defaultPerformKeyEquivalent:(NSEvent*)event { 380 return [super performKeyEquivalent:event]; 381} 382 383- (BOOL)defaultValidateUserInterfaceItem: 384 (id<NSValidatedUserInterfaceItem>)item { 385 return [super validateUserInterfaceItem:item]; 386} 387 388- (void)commandDispatch:(id)sender { 389 [_commandDispatcher dispatch:sender forHandler:_commandHandler]; 390} 391 392- (void)commandDispatchUsingKeyModifiers:(id)sender { 393 [_commandDispatcher dispatchUsingKeyModifiers:sender 394 forHandler:_commandHandler]; 395} 396 397// NSWindow overrides (NSUserInterfaceItemValidations implementation) 398 399- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { 400 return [_commandDispatcher validateUserInterfaceItem:item 401 forHandler:_commandHandler]; 402} 403 404// NSWindow overrides (NSAccessibility informal protocol implementation). 405 406- (id)accessibilityFocusedUIElement { 407 if (![self delegate]) 408 return [super accessibilityFocusedUIElement]; 409 410 // The SDK documents this as "The deepest descendant of the accessibility 411 // hierarchy that has the focus" and says "if a child element does not have 412 // the focus, either return self or, if available, invoke the superclass's 413 // implementation." 414 // The behavior of NSWindow is usually to return null, except when the window 415 // is first shown, when it returns self. But in the second case, we can 416 // provide richer a11y information by reporting the views::RootView instead. 417 // Additionally, if we don't do this, VoiceOver reads out the partial a11y 418 // properties on the NSWindow and repeats them when focusing an item in the 419 // RootView's a11y group. See http://crbug.com/748221. 420 id superFocus = [super accessibilityFocusedUIElement]; 421 if (!_bridge || superFocus != self) 422 return superFocus; 423 424 return _bridge->host_helper()->GetNativeViewAccessible(); 425} 426 427- (NSString*)accessibilityTitle { 428 // Check when NSWindow is asked for its title to provide the title given by 429 // the views::RootView (and WidgetDelegate::GetAccessibleWindowTitle()). For 430 // all other attributes, use what NSWindow provides by default since diverging 431 // from NSWindow's behavior can easily break VoiceOver integration. 432 NSString* viewsValue = self.rootAccessibilityObject.accessibilityTitle; 433 return viewsValue ? viewsValue : [super accessibilityTitle]; 434} 435 436@end 437