// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "ui/views/cocoa/bridged_native_widget.h" #import #include "base/logging.h" #import "base/mac/foundation_util.h" #include "base/mac/mac_util.h" #import "base/mac/sdk_forward_declarations.h" #include "base/thread_task_runner_handle.h" #include "ui/accelerated_widget_mac/window_resize_helper_mac.h" #import "ui/base/cocoa/constrained_window/constrained_window_animation.h" #include "ui/base/hit_test.h" #include "ui/base/ime/input_method.h" #include "ui/base/ime/input_method_factory.h" #include "ui/gfx/display.h" #include "ui/gfx/geometry/dip_util.h" #import "ui/gfx/mac/coordinate_conversion.h" #import "ui/gfx/mac/nswindow_frame_controls.h" #include "ui/gfx/screen.h" #import "ui/views/cocoa/bridged_content_view.h" #import "ui/views/cocoa/cocoa_mouse_capture.h" #include "ui/views/cocoa/tooltip_manager_mac.h" #import "ui/views/cocoa/views_nswindow_delegate.h" #import "ui/views/cocoa/widget_owner_nswindow_adapter.h" #include "ui/views/view.h" #include "ui/views/views_delegate.h" #include "ui/views/widget/native_widget_mac.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_aura_utils.h" #include "ui/views/widget/widget_delegate.h" extern "C" { typedef int32_t CGSConnection; CGSConnection _CGSDefaultConnection(); CGError CGSSetWindowBackgroundBlurRadius(CGSConnection connection, NSInteger windowNumber, int radius); } // The NSView that hosts the composited CALayer drawing the UI. It fills the // window but is not hittable so that accessibility hit tests always go to the // BridgedContentView. @interface ViewsCompositorSuperview : NSView @end @implementation ViewsCompositorSuperview - (NSView*)hitTest:(NSPoint)aPoint { return nil; } @end // This class overrides NSAnimation methods to invalidate the shadow for each // frame. It is required because the show animation uses CGSSetWindowWarp() // which is touchy about the consistency of the points it is given. The show // animation includes a translate, which fails to apply properly to the window // shadow, when that shadow is derived from a layer-hosting view. So invalidate // it. This invalidation is only needed to cater for the translate. It is not // required if CGSSetWindowWarp() is used in a way that keeps the center point // of the window stationary (e.g. a scale). It's also not required for the hide // animation: in that case, the shadow is never invalidated so retains the // shadow calculated before a translate is applied. @interface ModalShowAnimationWithLayer : ConstrainedWindowAnimationShow @end @implementation ModalShowAnimationWithLayer - (void)stopAnimation { [super stopAnimation]; [window_ invalidateShadow]; } - (void)setCurrentProgress:(NSAnimationProgress)progress { [super setCurrentProgress:progress]; [window_ invalidateShadow]; } @end namespace { const CGFloat kMavericksMenuOpacity = 251.0 / 255.0; const CGFloat kYosemiteMenuOpacity = 194.0 / 255.0; const int kYosemiteMenuBlur = 80; int kWindowPropertiesKey; float GetDeviceScaleFactorFromView(NSView* view) { gfx::Display display = gfx::Screen::GetScreenFor(view)->GetDisplayNearestWindow(view); DCHECK(display.is_valid()); return display.device_scale_factor(); } // Returns true if bounds passed to window in SetBounds should be treated as // though they are in screen coordinates. bool PositionWindowInScreenCoordinates(views::Widget* widget, views::Widget::InitParams::Type type) { // Replicate the logic in desktop_aura/desktop_screen_position_client.cc. if (views::GetAuraWindowTypeForWidgetType(type) == ui::wm::WINDOW_TYPE_POPUP) return true; return widget && widget->is_top_level(); } // Return the content size for a minimum or maximum widget size. gfx::Size GetClientSizeForWindowSize(NSWindow* window, const gfx::Size& window_size) { NSRect frame_rect = NSMakeRect(0, 0, window_size.width(), window_size.height()); // Note gfx::Size will prevent dimensions going negative. They are allowed to // be zero at this point, because Widget::GetMinimumSize() may later increase // the size. return gfx::Size([window contentRectForFrameRect:frame_rect].size); } BOOL WindowWantsMouseDownReposted(NSEvent* ns_event) { id delegate = [[ns_event window] delegate]; return [delegate respondsToSelector:@selector(shouldRepostPendingLeftMouseDown:)] && [delegate shouldRepostPendingLeftMouseDown:[ns_event locationInWindow]]; } // Check if a mouse-down event should drag the window. If so, repost the event. NSEvent* RepostEventIfHandledByWindow(NSEvent* ns_event) { enum RepostState { // Nothing reposted: hit-test new mouse-downs to see if they need to be // ignored and reposted after changing draggability. NONE, // Expecting the next event to be the reposted event: let it go through. EXPECTING_REPOST, // If, while reposting, another mousedown was received: when the reposted // event is seen, ignore it. REPOST_CANCELLED, }; // Which repost we're expecting to receive. static RepostState repost_state = NONE; // The event number of the reposted event. This let's us track whether an // event is actually the repost since user-generated events have increasing // event numbers. This is only valid while |repost_state != NONE|. static NSInteger reposted_event_number; NSInteger event_number = [ns_event eventNumber]; // The logic here is a bit convoluted because we want to mitigate race // conditions if somehow a different mouse-down occurs between reposts. // Specifically, we want to avoid: // - BridgedNativeWidget's draggability getting out of sync (e.g. if it is // draggable outside of a repost cycle), // - any repost loop. if (repost_state == NONE) { if (WindowWantsMouseDownReposted(ns_event)) { repost_state = EXPECTING_REPOST; reposted_event_number = event_number; CGEventPost(kCGSessionEventTap, [ns_event CGEvent]); return nil; } return ns_event; } if (repost_state == EXPECTING_REPOST) { // Call through so that the window is made non-draggable again. WindowWantsMouseDownReposted(ns_event); if (reposted_event_number == event_number) { // Reposted event received. repost_state = NONE; return nil; } // We were expecting a repost, but since this is a new mouse-down, cancel // reposting and allow event to continue as usual. repost_state = REPOST_CANCELLED; return ns_event; } DCHECK_EQ(REPOST_CANCELLED, repost_state); if (reposted_event_number == event_number) { // Reposting was cancelled, now that we've received the event, we don't // expect to see it again. repost_state = NONE; return nil; } return ns_event; } // Support window caption/draggable regions. // In AppKit, non-client regions are set by overriding // -[NSView mouseDownCanMoveWindow]. NSApplication caches this area as views are // installed and performs window moving when mouse-downs land in the area. // In Views, non-client regions are determined via hit-tests when the event // occurs. // To bridge the two models, we monitor mouse-downs with // +[NSEvent addLocalMonitorForEventsMatchingMask:handler:]. This receives // events after window dragging is handled, so for mouse-downs that land on a // draggable point, we cancel the event and repost it at the CGSessionEventTap // level so that window dragging will be handled again. void SetupDragEventMonitor() { static id monitor = nil; if (monitor) return; monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask handler:^NSEvent*(NSEvent* ns_event) { return RepostEventIfHandledByWindow(ns_event); }]; } // Returns a task runner for creating a ui::Compositor. This allows compositor // tasks to be funneled through ui::WindowResizeHelper's task runner to allow // resize operations to coordinate with frames provided by the GPU process. scoped_refptr GetCompositorTaskRunner() { // If the WindowResizeHelper's pumpable task runner is set, it means the GPU // process is directing messages there, and the compositor can synchronize // with it. Otherwise, just use the UI thread. scoped_refptr task_runner = ui::WindowResizeHelperMac::Get()->task_runner(); return task_runner ? task_runner : base::ThreadTaskRunnerHandle::Get(); } } // namespace namespace views { // static gfx::Size BridgedNativeWidget::GetWindowSizeForClientSize( NSWindow* window, const gfx::Size& content_size) { NSRect content_rect = NSMakeRect(0, 0, content_size.width(), content_size.height()); NSRect frame_rect = [window frameRectForContentRect:content_rect]; return gfx::Size(NSWidth(frame_rect), NSHeight(frame_rect)); } BridgedNativeWidget::BridgedNativeWidget(NativeWidgetMac* parent) : native_widget_mac_(parent), focus_manager_(nullptr), widget_type_(Widget::InitParams::TYPE_WINDOW), // Updated in Init(). parent_(nullptr), target_fullscreen_state_(false), in_fullscreen_transition_(false), window_visible_(false), wants_to_be_visible_(false) { SetupDragEventMonitor(); DCHECK(parent); window_delegate_.reset( [[ViewsNSWindowDelegate alloc] initWithBridgedNativeWidget:this]); } BridgedNativeWidget::~BridgedNativeWidget() { RemoveOrDestroyChildren(); DCHECK(child_windows_.empty()); SetFocusManager(NULL); SetRootView(NULL); DestroyCompositor(); if ([window_ delegate]) { // If the delegate is still set on a modal dialog, it means it was not // closed via [NSApplication endSheet:]. This is probably OK if the widget // was never shown. But Cocoa ignores close() calls on open sheets. Calling // endSheet: here would work, but it messes up assumptions elsewhere. E.g. // DialogClientView assumes its delegate is alive when closing, which isn't // true after endSheet: synchronously calls OnNativeWidgetDestroyed(). // So ban it. Modal dialogs should be closed via Widget::Close(). DCHECK(!native_widget_mac_->IsWindowModalSheet()); // If the delegate is still set, it means OnWindowWillClose has not been // called and the window is still open. Calling -[NSWindow close] will // synchronously call OnWindowWillClose and notify NativeWidgetMac. [window_ close]; } DCHECK(![window_ delegate]); } void BridgedNativeWidget::Init(base::scoped_nsobject window, const Widget::InitParams& params) { widget_type_ = params.type; DCHECK(!window_); window_.swap(window); [window_ setDelegate:window_delegate_]; // Register for application hide notifications so that visibility can be // properly tracked. This is not done in the delegate so that the lifetime is // tied to the C++ object, rather than the delegate (which may be reference // counted). This is required since the application hides do not send an // orderOut: to individual windows. Unhide, however, does send an order // message. [[NSNotificationCenter defaultCenter] addObserver:window_delegate_ selector:@selector(onWindowOrderChanged:) name:NSApplicationDidHideNotification object:nil]; // Validate the window's initial state, otherwise the bridge's initial // tracking state will be incorrect. DCHECK(![window_ isVisible]); DCHECK_EQ(0u, [window_ styleMask] & NSFullScreenWindowMask); if (params.parent) { // Disallow creating child windows of views not currently in an NSWindow. CHECK([params.parent window]); BridgedNativeWidget* bridged_native_widget_parent = NativeWidgetMac::GetBridgeForNativeWindow([params.parent window]); // If the parent is another BridgedNativeWidget, just add to the collection // of child windows it owns and manages. Otherwise, create an adapter to // anchor the child widget and observe when the parent NSWindow is closed. if (bridged_native_widget_parent) { parent_ = bridged_native_widget_parent; bridged_native_widget_parent->child_windows_.push_back(this); } else { parent_ = new WidgetOwnerNSWindowAdapter(this, params.parent); } } // OSX likes to put shadows on most things. However, frameless windows (with // styleMask = NSBorderlessWindowMask) default to no shadow. So change that. // SHADOW_TYPE_DROP is used for Menus, which get the same shadow style on Mac. switch (params.shadow_type) { case Widget::InitParams::SHADOW_TYPE_NONE: [window_ setHasShadow:NO]; break; case Widget::InitParams::SHADOW_TYPE_DEFAULT: case Widget::InitParams::SHADOW_TYPE_DROP: [window_ setHasShadow:YES]; break; } // No default case, to pick up new types. // Set a meaningful initial bounds. Note that except for frameless widgets // with no WidgetDelegate, the bounds will be set again by Widget after // initializing the non-client view. In the former case, if bounds were not // set at all, the creator of the Widget is expected to call SetBounds() // before calling Widget::Show() to avoid a kWindowSizeDeterminedLater-sized // (i.e. 1x1) window appearing. if (!params.bounds.IsEmpty()) { SetBounds(params.bounds); } else { // If a position is set, but no size, complain. Otherwise, a 1x1 window // would appear there, which might be unexpected. DCHECK(params.bounds.origin().IsOrigin()) << "Zero-sized windows not supported on Mac."; // Otherwise, bounds is all zeroes. Cocoa will currently have the window at // the bottom left of the screen. To support a client calling SetSize() only // (and for consistency across platforms) put it at the top-left instead. // Read back the current frame: it will be a 1x1 context rect but the frame // size also depends on the window style. NSRect frame_rect = [window_ frame]; SetBounds(gfx::Rect(gfx::Point(), gfx::Size(NSWidth(frame_rect), NSHeight(frame_rect)))); } // Widgets for UI controls (usually layered above web contents) start visible. if (params.type == Widget::InitParams::TYPE_CONTROL) SetVisibilityState(SHOW_INACTIVE); // Tooltip Widgets shouldn't have their own tooltip manager, but tooltips are // native on Mac, so nothing should ever want one in Widget form. DCHECK_NE(params.type, Widget::InitParams::TYPE_TOOLTIP); tooltip_manager_.reset(new TooltipManagerMac(this)); } void BridgedNativeWidget::SetFocusManager(FocusManager* focus_manager) { if (focus_manager_ == focus_manager) return; if (focus_manager_) focus_manager_->RemoveFocusChangeListener(this); if (focus_manager) focus_manager->AddFocusChangeListener(this); focus_manager_ = focus_manager; } void BridgedNativeWidget::SetBounds(const gfx::Rect& new_bounds) { Widget* widget = native_widget_mac_->GetWidget(); // -[NSWindow contentMinSize] is only checked by Cocoa for user-initiated // resizes. This is not what toolkit-views expects, so clamp. Note there is // no check for maximum size (consistent with aura::Window::SetBounds()). gfx::Size clamped_content_size = GetClientSizeForWindowSize(window_, new_bounds.size()); clamped_content_size.SetToMax(widget->GetMinimumSize()); // A contentRect with zero width or height is a banned practice in ChromeMac, // due to unpredictable OSX treatment. DCHECK(!clamped_content_size.IsEmpty()) << "Zero-sized windows not supported on Mac"; if (!window_visible_ && native_widget_mac_->IsWindowModalSheet()) { // Window-Modal dialogs (i.e. sheets) are positioned by Cocoa when shown for // the first time. They also have no frame, so just update the content size. [window_ setContentSize:NSMakeSize(clamped_content_size.width(), clamped_content_size.height())]; return; } gfx::Rect actual_new_bounds( new_bounds.origin(), GetWindowSizeForClientSize(window_, clamped_content_size)); if (parent_ && !PositionWindowInScreenCoordinates(widget, widget_type_)) actual_new_bounds.Offset(parent_->GetChildWindowOffset()); [window_ setFrame:gfx::ScreenRectToNSRect(actual_new_bounds) display:YES animate:NO]; } void BridgedNativeWidget::SetRootView(views::View* view) { if (view == [bridged_view_ hostedView]) return; // If this is ever false, the compositor will need to be properly torn down // and replaced, pointing at the new view. DCHECK(!view || !compositor_widget_); [bridged_view_ clearView]; bridged_view_.reset(); // Note that there can still be references to the old |bridged_view_| // floating around in Cocoa libraries at this point. However, references to // the old views::View will be gone, so any method calls will become no-ops. if (view) { bridged_view_.reset([[BridgedContentView alloc] initWithView:view]); // Objective C initializers can return nil. However, if |view| is non-NULL // this should be treated as an error and caught early. CHECK(bridged_view_); } [window_ setContentView:bridged_view_]; } void BridgedNativeWidget::SetVisibilityState(WindowVisibilityState new_state) { // Ensure that: // - A window with an invisible parent is not made visible. // - A parent changing visibility updates child window visibility. // * But only when changed via this function - ignore changes via the // NSWindow API, or changes propagating out from here. wants_to_be_visible_ = new_state != HIDE_WINDOW; if (new_state == HIDE_WINDOW) { [window_ orderOut:nil]; DCHECK(!window_visible_); return; } DCHECK(wants_to_be_visible_); // If the parent (or an ancestor) is hidden, return and wait for it to become // visible. if (parent() && !parent()->IsVisibleParent()) return; if (native_widget_mac_->IsWindowModalSheet()) { ShowAsModalSheet(); return; } if (new_state == SHOW_AND_ACTIVATE_WINDOW) { [window_ makeKeyAndOrderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; } else { // ui::SHOW_STATE_INACTIVE is typically used to avoid stealing focus from a // parent window. So, if there's a parent, order above that. Otherwise, this // will order above all windows at the same level. NSInteger parent_window_number = 0; if (parent_) parent_window_number = [parent_->GetNSWindow() windowNumber]; [window_ orderWindow:NSWindowAbove relativeTo:parent_window_number]; } DCHECK(window_visible_); // For non-sheet modal types, use the constrained window animations to make // the window appear. if (native_widget_mac_->GetWidget()->IsModal()) { base::scoped_nsobject show_animation( [[ModalShowAnimationWithLayer alloc] initWithWindow:window_]); // The default mode is blocking, which would block the UI thread for the // duration of the animation, but would keep it smooth. The window also // hasn't yet received a frame from the compositor at this stage, so it is // fully transparent until the GPU sends a frame swap IPC. For the blocking // option, the animation needs to wait until AcceleratedWidgetSwapCompleted // has been called at least once, otherwise it will animate nothing. [show_animation setAnimationBlockingMode:NSAnimationNonblocking]; [show_animation startAnimation]; } } void BridgedNativeWidget::AcquireCapture() { DCHECK(!HasCapture()); if (!window_visible_) return; // Capture on hidden windows is disallowed. mouse_capture_.reset(new CocoaMouseCapture(this)); // Initiating global event capture with addGlobalMonitorForEventsMatchingMask: // will reset the mouse cursor to an arrow. Asking the window for an update // here will restore what we want. However, it can sometimes cause the cursor // to flicker, once, on the initial mouseDown. // TOOD(tapted): Make this unnecessary by only asking for global mouse capture // for the cases that need it (e.g. menus, but not drag and drop). [window_ cursorUpdate:[NSApp currentEvent]]; } void BridgedNativeWidget::ReleaseCapture() { mouse_capture_.reset(); } bool BridgedNativeWidget::HasCapture() { return mouse_capture_ && mouse_capture_->IsActive(); } void BridgedNativeWidget::SetNativeWindowProperty(const char* name, void* value) { NSString* key = [NSString stringWithUTF8String:name]; if (value) { [GetWindowProperties() setObject:[NSValue valueWithPointer:value] forKey:key]; } else { [GetWindowProperties() removeObjectForKey:key]; } } void* BridgedNativeWidget::GetNativeWindowProperty(const char* name) const { NSString* key = [NSString stringWithUTF8String:name]; return [[GetWindowProperties() objectForKey:key] pointerValue]; } void BridgedNativeWidget::SetCursor(NSCursor* cursor) { [window_delegate_ setCursor:cursor]; } void BridgedNativeWidget::OnWindowWillClose() { if (parent_) { parent_->RemoveChildWindow(this); parent_ = nullptr; } [window_ setDelegate:nil]; [[NSNotificationCenter defaultCenter] removeObserver:window_delegate_]; native_widget_mac_->OnWindowWillClose(); } void BridgedNativeWidget::OnFullscreenTransitionStart( bool target_fullscreen_state) { // Note: This can fail for fullscreen changes started externally, but a user // shouldn't be able to do that if the window is invisible to begin with. DCHECK(window_visible_); DCHECK_NE(target_fullscreen_state, target_fullscreen_state_); target_fullscreen_state_ = target_fullscreen_state; in_fullscreen_transition_ = true; // If going into fullscreen, store an answer for GetRestoredBounds(). if (target_fullscreen_state) bounds_before_fullscreen_ = gfx::ScreenRectFromNSRect([window_ frame]); } void BridgedNativeWidget::OnFullscreenTransitionComplete( bool actual_fullscreen_state) { in_fullscreen_transition_ = false; if (target_fullscreen_state_ == actual_fullscreen_state) { // Ensure constraints are re-applied when completing a transition. OnSizeConstraintsChanged(); return; } // First update to reflect reality so that OnTargetFullscreenStateChanged() // expects the change. target_fullscreen_state_ = actual_fullscreen_state; ToggleDesiredFullscreenState(); // Usually ToggleDesiredFullscreenState() sets |in_fullscreen_transition_| via // OnFullscreenTransitionStart(). When it does not, it means Cocoa ignored the // toggleFullScreen: request. This can occur when the fullscreen transition // fails and Cocoa is *about* to send windowDidFailToEnterFullScreen:. // Annoyingly, for this case, Cocoa first sends windowDidExitFullScreen:. if (in_fullscreen_transition_) DCHECK_NE(target_fullscreen_state_, actual_fullscreen_state); } void BridgedNativeWidget::ToggleDesiredFullscreenState() { // If there is currently an animation into or out of fullscreen, then AppKit // emits the string "not in fullscreen state" to stdio and does nothing. For // this case, schedule a transition back into the desired state when the // animation completes. if (in_fullscreen_transition_) { target_fullscreen_state_ = !target_fullscreen_state_; return; } // Going fullscreen implicitly makes the window visible. AppKit does this. // That is, -[NSWindow isVisible] is always true after a call to -[NSWindow // toggleFullScreen:]. Unfortunately, this change happens after AppKit calls // -[NSWindowDelegate windowWillEnterFullScreen:], and AppKit doesn't send an // orderWindow message. So intercepting the implicit change is hard. // Luckily, to trigger externally, the window typically needs to be visible in // the first place. So we can just ensure the window is visible here instead // of relying on AppKit to do it, and not worry that OnVisibilityChanged() // won't be called for externally triggered fullscreen requests. if (!window_visible_) SetVisibilityState(SHOW_INACTIVE); if (base::mac::IsOSSnowLeopard()) { NOTIMPLEMENTED(); return; // TODO(tapted): Implement this for Snow Leopard. } // Enable fullscreen collection behavior because: // 1: -[NSWindow toggleFullscreen:] would otherwise be ignored, // 2: the fullscreen button must be enabled so the user can leave fullscreen. // This will be reset when a transition out of fullscreen completes. gfx::SetNSWindowCanFullscreen(window_, true); [window_ toggleFullScreen:nil]; } void BridgedNativeWidget::OnSizeChanged() { gfx::Size new_size = GetClientAreaSize(); native_widget_mac_->GetWidget()->OnNativeWidgetSizeChanged(new_size); if (layer()) { UpdateLayerProperties(); if ([window_ inLiveResize]) MaybeWaitForFrame(new_size); } } void BridgedNativeWidget::OnVisibilityChanged() { OnVisibilityChangedTo([window_ isVisible]); } void BridgedNativeWidget::OnVisibilityChangedTo(bool new_visibility) { if (window_visible_ == new_visibility) return; window_visible_ = new_visibility; // If arriving via SetVisible(), |wants_to_be_visible_| should already be set. // If made visible externally (e.g. Cmd+H), just roll with it. Don't try (yet) // to distinguish being *hidden* externally from being hidden by a parent // window - we might not need that. if (window_visible_) { wants_to_be_visible_ = true; if (parent_) [parent_->GetNSWindow() addChildWindow:window_ ordered:NSWindowAbove]; } else { mouse_capture_.reset(); // Capture on hidden windows is not permitted. // When becoming invisible, remove the entry in any parent's childWindow // list. Cocoa's childWindow management breaks down when child windows are // hidden. if (parent_) [parent_->GetNSWindow() removeChildWindow:window_]; } // TODO(tapted): Investigate whether we want this for Mac. This is what Aura // does, and it is what tests expect. However, because layer drawing is // asynchronous (and things like deminiaturize in AppKit are not), it can // result in a CALayer appearing on screen before it has been redrawn in the // GPU process. This is a general problem. In content, a helper class, // RenderWidgetResizeHelper, blocks the UI thread in -[NSView setFrameSize:] // and RenderWidgetHostView::Show() until a frame is ready. if (layer()) { layer()->SetVisible(window_visible_); layer()->SchedulePaint(gfx::Rect(GetClientAreaSize())); } NotifyVisibilityChangeDown(); native_widget_mac_->GetWidget()->OnNativeWidgetVisibilityChanged( window_visible_); // Toolkit-views suppresses redraws while not visible. To prevent Cocoa asking // for an "empty" draw, disable auto-display while hidden. For example, this // prevents Cocoa drawing just *after* a minimize, resulting in a blank window // represented in the deminiaturize animation. [window_ setAutodisplay:window_visible_]; } void BridgedNativeWidget::OnBackingPropertiesChanged() { if (layer()) UpdateLayerProperties(); } void BridgedNativeWidget::OnWindowKeyStatusChangedTo(bool is_key) { Widget* widget = native_widget_mac()->GetWidget(); widget->OnNativeWidgetActivationChanged(is_key); // The contentView is the BridgedContentView hosting the views::RootView. The // focus manager will already know if a native subview has focus. if ([window_ contentView] == [window_ firstResponder]) { if (is_key) { widget->OnNativeFocus(); widget->GetFocusManager()->RestoreFocusedView(); } else { widget->OnNativeBlur(); widget->GetFocusManager()->StoreFocusedView(true); } } } bool BridgedNativeWidget::ShouldRepostPendingLeftMouseDown( NSPoint location_in_window) { if (!bridged_view_) return false; if ([bridged_view_ mouseDownCanMoveWindow]) { // This is a re-post, the movement has already started, so we can make the // window non-draggable again. SetDraggable(false); return false; } gfx::Point point(location_in_window.x, NSHeight([window_ frame]) - location_in_window.y); bool should_move_window = native_widget_mac()->GetWidget()->GetNonClientComponent(point) == HTCAPTION; // Check that the point is not obscured by non-content NSViews. for (NSView* subview : [[bridged_view_ superview] subviews]) { if (subview == bridged_view_.get()) continue; if (![subview mouseDownCanMoveWindow] && NSPointInRect(location_in_window, [subview frame])) { should_move_window = false; break; } } if (!should_move_window) return false; // Make the window draggable, then return true to repost the event. SetDraggable(true); return true; } void BridgedNativeWidget::OnSizeConstraintsChanged() { // Don't modify the size constraints or fullscreen collection behavior while // in fullscreen or during a transition. OnFullscreenTransitionComplete will // reset these after leaving fullscreen. if (target_fullscreen_state_ || in_fullscreen_transition_) return; Widget* widget = native_widget_mac()->GetWidget(); gfx::Size min_size = widget->GetMinimumSize(); gfx::Size max_size = widget->GetMaximumSize(); bool is_resizable = widget->widget_delegate()->CanResize(); bool shows_resize_controls = is_resizable && (min_size.IsEmpty() || min_size != max_size); bool shows_fullscreen_controls = is_resizable && widget->widget_delegate()->CanMaximize(); gfx::ApplyNSWindowSizeConstraints(window_, min_size, max_size, shows_resize_controls, shows_fullscreen_controls); } ui::InputMethod* BridgedNativeWidget::GetInputMethod() { if (!input_method_) { input_method_ = ui::CreateInputMethod(this, nil); // For now, use always-focused mode on Mac for the input method. // TODO(tapted): Move this to OnWindowKeyStatusChangedTo() and balance. input_method_->OnFocus(); } return input_method_.get(); } gfx::Rect BridgedNativeWidget::GetRestoredBounds() const { if (target_fullscreen_state_ || in_fullscreen_transition_) return bounds_before_fullscreen_; return gfx::ScreenRectFromNSRect([window_ frame]); } void BridgedNativeWidget::CreateLayer(ui::LayerType layer_type, bool translucent) { DCHECK(bridged_view_); DCHECK(!layer()); CreateCompositor(); DCHECK(compositor_); SetLayer(new ui::Layer(layer_type)); // Note, except for controls, this will set the layer to be hidden, since it // is only called during Init(). layer()->SetVisible(window_visible_); layer()->set_delegate(this); InitCompositor(); // Transparent window support. layer()->GetCompositor()->SetHostHasTransparentBackground(translucent); layer()->SetFillsBoundsOpaquely(!translucent); // Use the regular window background for window modal sheets. The layer() will // still paint over most of it, but the native -[NSApp beginSheet:] animation // blocks the UI thread, so there's no way to invalidate the shadow to match // the composited layer. This assumes the native window shape is a good match // for the composited NonClientFrameView, which should be the case since the // native shape is what's most appropriate for displaying sheets on Mac. if (translucent && !native_widget_mac_->IsWindowModalSheet()) { [window_ setOpaque:NO]; [window_ setBackgroundColor:[NSColor clearColor]]; } UpdateLayerProperties(); } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, internal::InputMethodDelegate: ui::EventDispatchDetails BridgedNativeWidget::DispatchKeyEventPostIME( ui::KeyEvent* key) { DCHECK(focus_manager_); native_widget_mac_->GetWidget()->OnKeyEvent(key); if (!key->handled()) { if (!focus_manager_->OnKeyEvent(*key)) key->StopPropagation(); } return ui::EventDispatchDetails(); } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, CocoaMouseCaptureDelegate: void BridgedNativeWidget::PostCapturedEvent(NSEvent* event) { [bridged_view_ processCapturedMouseEvent:event]; } void BridgedNativeWidget::OnMouseCaptureLost() { native_widget_mac_->GetWidget()->OnMouseCaptureLost(); } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, FocusChangeListener: void BridgedNativeWidget::OnWillChangeFocus(View* focused_before, View* focused_now) { } void BridgedNativeWidget::OnDidChangeFocus(View* focused_before, View* focused_now) { ui::InputMethod* input_method = native_widget_mac_->GetWidget()->GetInputMethod(); if (input_method) { ui::TextInputClient* input_client = input_method->GetTextInputClient(); [bridged_view_ setTextInputClient:input_client]; } } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, LayerDelegate: void BridgedNativeWidget::OnPaintLayer(const ui::PaintContext& context) { DCHECK(window_visible_); native_widget_mac_->GetWidget()->OnNativeWidgetPaint(context); } void BridgedNativeWidget::OnDelegatedFrameDamage( const gfx::Rect& damage_rect_in_dip) { NOTIMPLEMENTED(); } void BridgedNativeWidget::OnDeviceScaleFactorChanged( float device_scale_factor) { NOTIMPLEMENTED(); } base::Closure BridgedNativeWidget::PrepareForLayerBoundsChange() { NOTIMPLEMENTED(); return base::Closure(); } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, AcceleratedWidgetMac: NSView* BridgedNativeWidget::AcceleratedWidgetGetNSView() const { return compositor_superview_; } bool BridgedNativeWidget::AcceleratedWidgetShouldIgnoreBackpressure() const { return true; } void BridgedNativeWidget::AcceleratedWidgetGetVSyncParameters( base::TimeTicks* timebase, base::TimeDelta* interval) const { // TODO(tapted): Add vsync support. *timebase = base::TimeTicks(); *interval = base::TimeDelta(); } void BridgedNativeWidget::AcceleratedWidgetSwapCompleted( const std::vector& latency_info) { // Ignore frames arriving "late" for an old size. A frame at the new size // should arrive soon. if (!compositor_widget_->HasFrameOfSize(GetClientAreaSize())) return; if (invalidate_shadow_on_frame_swap_) { invalidate_shadow_on_frame_swap_ = false; [window_ invalidateShadow]; } } void BridgedNativeWidget::AcceleratedWidgetHitError() { compositor_->ScheduleFullRedraw(); } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, BridgedNativeWidgetOwner: NSWindow* BridgedNativeWidget::GetNSWindow() { return window_; } gfx::Vector2d BridgedNativeWidget::GetChildWindowOffset() const { return gfx::ScreenRectFromNSRect([window_ frame]).OffsetFromOrigin(); } bool BridgedNativeWidget::IsVisibleParent() const { return parent_ ? window_visible_ && parent_->IsVisibleParent() : window_visible_; } void BridgedNativeWidget::RemoveChildWindow(BridgedNativeWidget* child) { auto location = std::find( child_windows_.begin(), child_windows_.end(), child); DCHECK(location != child_windows_.end()); child_windows_.erase(location); // Note the child is sometimes removed already by AppKit. This depends on OS // version, and possibly some unpredictable reference counting. Removing it // here should be safe regardless. [window_ removeChildWindow:child->window_]; } //////////////////////////////////////////////////////////////////////////////// // BridgedNativeWidget, private: void BridgedNativeWidget::RemoveOrDestroyChildren() { // TODO(tapted): Implement unowned child windows if required. while (!child_windows_.empty()) { // The NSWindow can only be destroyed after -[NSWindow close] is complete. // Retain the window, otherwise the reference count can reach zero when the // child calls back into RemoveChildWindow() via its OnWindowWillClose(). base::scoped_nsobject child( [child_windows_.back()->ns_window() retain]); [child close]; } } void BridgedNativeWidget::NotifyVisibilityChangeDown() { // Child windows sometimes like to close themselves in response to visibility // changes. That's supported, but only with the asynchronous Widget::Close(). // Perform a heuristic to detect child removal that would break these loops. const size_t child_count = child_windows_.size(); if (!window_visible_) { for (BridgedNativeWidget* child : child_windows_) { if (child->window_visible_) [child->ns_window() orderOut:nil]; DCHECK(!child->window_visible_); CHECK_EQ(child_count, child_windows_.size()); } // The orderOut calls above should result in a call to OnVisibilityChanged() // in each child. There, children will remove themselves from the NSWindow // childWindow list as well as propagate NotifyVisibilityChangeDown() calls // to any children of their own. DCHECK_EQ(0u, [[window_ childWindows] count]); return; } NSUInteger visible_children = 0; // For a DCHECK below. NSInteger parent_window_number = [window_ windowNumber]; for (BridgedNativeWidget* child: child_windows_) { // Note: order the child windows on top, regardless of whether or not they // are currently visible. They probably aren't, since the parent was hidden // prior to this, but they could have been made visible in other ways. if (child->wants_to_be_visible_) { ++visible_children; // Here -[NSWindow orderWindow:relativeTo:] is used to put the window on // screen. However, that by itself is insufficient to guarantee a correct // z-order relationship. If this function is being called from a z-order // change in the parent, orderWindow turns out to be unreliable (i.e. the // ordering doesn't always take effect). What this actually relies on is // the resulting call to OnVisibilityChanged() in the child, which will // then insert itself into -[NSWindow childWindows] to let Cocoa do its // internal layering magic. [child->ns_window() orderWindow:NSWindowAbove relativeTo:parent_window_number]; DCHECK(child->window_visible_); } CHECK_EQ(child_count, child_windows_.size()); } DCHECK_EQ(visible_children, [[window_ childWindows] count]); } gfx::Size BridgedNativeWidget::GetClientAreaSize() const { NSRect content_rect = [window_ contentRectForFrameRect:[window_ frame]]; return gfx::Size(NSWidth(content_rect), NSHeight(content_rect)); } void BridgedNativeWidget::CreateCompositor() { DCHECK(!compositor_); DCHECK(!compositor_widget_); DCHECK(ViewsDelegate::GetInstance()); ui::ContextFactory* context_factory = ViewsDelegate::GetInstance()->GetContextFactory(); DCHECK(context_factory); AddCompositorSuperview(); // TODO(tapted): Get this value from GpuDataManagerImpl via ViewsDelegate. bool needs_gl_finish_workaround = false; compositor_widget_.reset( new ui::AcceleratedWidgetMac(needs_gl_finish_workaround)); compositor_.reset( new ui::Compositor(context_factory, GetCompositorTaskRunner())); compositor_->SetAcceleratedWidget(compositor_widget_->accelerated_widget()); compositor_widget_->SetNSView(this); } void BridgedNativeWidget::InitCompositor() { DCHECK(layer()); float scale_factor = GetDeviceScaleFactorFromView(compositor_superview_); gfx::Size size_in_dip = GetClientAreaSize(); compositor_->SetScaleAndSize(scale_factor, ConvertSizeToPixel(scale_factor, size_in_dip)); compositor_->SetRootLayer(layer()); } void BridgedNativeWidget::DestroyCompositor() { if (layer()) { // LayerOwner supports a change in ownership, e.g., to animate a closing // window, but that won't work as expected for the root layer in // BridgedNativeWidget. DCHECK_EQ(this, layer()->owner()); layer()->CompleteAllAnimations(); layer()->SuppressPaint(); layer()->set_delegate(nullptr); } DestroyLayer(); if (!compositor_widget_) { DCHECK(!compositor_); return; } compositor_widget_->ResetNSView(); compositor_.reset(); compositor_widget_.reset(); } void BridgedNativeWidget::AddCompositorSuperview() { DCHECK(!compositor_superview_); compositor_superview_.reset( [[ViewsCompositorSuperview alloc] initWithFrame:[bridged_view_ bounds]]); // Size and resize automatically with |bridged_view_|. [compositor_superview_ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; // Enable HiDPI backing when supported (only on 10.7+). if ([compositor_superview_ respondsToSelector: @selector(setWantsBestResolutionOpenGLSurface:)]) { [compositor_superview_ setWantsBestResolutionOpenGLSurface:YES]; } base::scoped_nsobject background_layer([[CALayer alloc] init]); [background_layer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; if (widget_type_ == Widget::InitParams::TYPE_MENU) { // Giving the canvas opacity messes up subpixel font rendering, so use a // solid background, but make the CALayer transparent. if (base::mac::IsOSYosemiteOrLater()) { [background_layer setOpacity:kYosemiteMenuOpacity]; CGSSetWindowBackgroundBlurRadius( _CGSDefaultConnection(), [window_ windowNumber], kYosemiteMenuBlur); // The blur effect does not occur with a fully transparent (or fully // layer-backed) window. Setting a window background will use square // corners, so ask the contentView to draw one instead. [bridged_view_ setDrawMenuBackgroundForBlur:YES]; } else { [background_layer setOpacity:kMavericksMenuOpacity]; } } // Set the layer first to create a layer-hosting view (not layer-backed). [compositor_superview_ setLayer:background_layer]; [compositor_superview_ setWantsLayer:YES]; // The UI compositor should always be the first subview, to ensure webviews // are drawn on top of it. DCHECK_EQ(0u, [[bridged_view_ subviews] count]); [bridged_view_ addSubview:compositor_superview_]; } void BridgedNativeWidget::UpdateLayerProperties() { DCHECK(layer()); DCHECK(compositor_superview_); gfx::Size size_in_dip = GetClientAreaSize(); layer()->SetBounds(gfx::Rect(size_in_dip)); float scale_factor = GetDeviceScaleFactorFromView(compositor_superview_); compositor_->SetScaleAndSize(scale_factor, ConvertSizeToPixel(scale_factor, size_in_dip)); // For a translucent window, the shadow calculation needs to be carried out // after the frame from the compositor arrives. if (![window_ isOpaque]) invalidate_shadow_on_frame_swap_ = true; } void BridgedNativeWidget::MaybeWaitForFrame(const gfx::Size& size_in_dip) { if (!layer()->IsDrawn() || compositor_widget_->HasFrameOfSize(size_in_dip)) return; const int kPaintMsgTimeoutMS = 50; const base::TimeTicks start_time = base::TimeTicks::Now(); const base::TimeTicks timeout_time = start_time + base::TimeDelta::FromMilliseconds(kPaintMsgTimeoutMS); ui::WindowResizeHelperMac* resize_helper = ui::WindowResizeHelperMac::Get(); for (base::TimeTicks now = start_time; now < timeout_time; now = base::TimeTicks::Now()) { if (!resize_helper->WaitForSingleTaskToRun(timeout_time - now)) return; // Timeout. // Since the UI thread is blocked, the size shouldn't change. DCHECK(size_in_dip == GetClientAreaSize()); if (compositor_widget_->HasFrameOfSize(size_in_dip)) return; // Frame arrived. } } void BridgedNativeWidget::ShowAsModalSheet() { // -[NSApp beginSheet:] will block the UI thread while the animation runs. // So that it doesn't animate a fully transparent window, first wait for a // frame. The first step is to pretend that the window is already visible. window_visible_ = true; layer()->SetVisible(true); native_widget_mac_->GetWidget()->OnNativeWidgetVisibilityChanged(true); MaybeWaitForFrame(GetClientAreaSize()); NSWindow* parent_window = parent_->GetNSWindow(); DCHECK(parent_window); [NSApp beginSheet:window_ modalForWindow:parent_window modalDelegate:[window_ delegate] didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nullptr]; } NSMutableDictionary* BridgedNativeWidget::GetWindowProperties() const { NSMutableDictionary* properties = objc_getAssociatedObject( window_, &kWindowPropertiesKey); if (!properties) { properties = [NSMutableDictionary dictionary]; objc_setAssociatedObject(window_, &kWindowPropertiesKey, properties, OBJC_ASSOCIATION_RETAIN); } return properties; } void BridgedNativeWidget::SetDraggable(bool draggable) { [bridged_view_ setMouseDownCanMoveWindow:draggable]; // AppKit will not update its cache of mouseDownCanMoveWindow unless something // changes. Previously we tried adding an NSView and removing it, but for some // reason it required reposting the mouse-down event, and didn't always work. // Calling the below seems to be an effective solution. [window_ setMovableByWindowBackground:NO]; [window_ setMovableByWindowBackground:YES]; } } // namespace views