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