// Copyright (c) 2013 GitHub, Inc. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. #include "shell/browser/native_window_mac.h" #include #include #include #include #include #include #include #include "base/apple/scoped_cftyperef.h" #include "base/mac/mac_util.h" #include "base/strings/sys_string_conversions.h" #include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" #include "components/remote_cocoa/browser/scoped_cg_window_id.h" #include "content/public/browser/browser_accessibility_state.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/desktop_media_id.h" #include "shell/browser/browser.h" #include "shell/browser/javascript_environment.h" #include "shell/browser/native_browser_view_mac.h" #include "shell/browser/ui/cocoa/electron_native_widget_mac.h" #include "shell/browser/ui/cocoa/electron_ns_window.h" #include "shell/browser/ui/cocoa/electron_ns_window_delegate.h" #include "shell/browser/ui/cocoa/electron_preview_item.h" #include "shell/browser/ui/cocoa/electron_touch_bar.h" #include "shell/browser/ui/cocoa/root_view_mac.h" #include "shell/browser/ui/cocoa/window_buttons_proxy.h" #include "shell/browser/ui/drag_util.h" #include "shell/browser/ui/inspectable_web_contents.h" #include "shell/browser/window_list.h" #include "shell/common/gin_converters/gfx_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/node_includes.h" #include "shell/common/options_switches.h" #include "skia/ext/skia_utils_mac.h" #include "third_party/skia/include/core/SkRegion.h" #include "third_party/webrtc/modules/desktop_capture/mac/window_list_utils.h" #include "ui/base/hit_test.h" #include "ui/display/screen.h" #include "ui/gfx/skia_util.h" #include "ui/gl/gpu_switching_manager.h" #include "ui/views/background.h" #include "ui/views/cocoa/native_widget_mac_ns_window_host.h" #include "ui/views/widget/widget.h" #include "ui/views/window/native_frame_view_mac.h" @interface ElectronProgressBar : NSProgressIndicator @end @implementation ElectronProgressBar - (void)drawRect:(NSRect)dirtyRect { if (self.style != NSProgressIndicatorStyleBar) return; // Draw edges of rounded rect. NSRect rect = NSInsetRect([self bounds], 1.0, 1.0); CGFloat radius = rect.size.height / 2; NSBezierPath* bezier_path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; [bezier_path setLineWidth:2.0]; [[NSColor grayColor] set]; [bezier_path stroke]; // Fill the rounded rect. rect = NSInsetRect(rect, 2.0, 2.0); radius = rect.size.height / 2; bezier_path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; [bezier_path setLineWidth:1.0]; [bezier_path addClip]; // Calculate the progress width. rect.size.width = floor(rect.size.width * ([self doubleValue] / [self maxValue])); // Fill the progress bar with color blue. [[NSColor colorWithSRGBRed:0.2 green:0.6 blue:1 alpha:1] set]; NSRectFill(rect); } @end namespace gin { template <> struct Converter { static bool FromV8(v8::Isolate* isolate, v8::Handle val, electron::NativeWindowMac::VisualEffectState* out) { using VisualEffectState = electron::NativeWindowMac::VisualEffectState; std::string visual_effect_state; if (!ConvertFromV8(isolate, val, &visual_effect_state)) return false; if (visual_effect_state == "followWindow") { *out = VisualEffectState::kFollowWindow; } else if (visual_effect_state == "active") { *out = VisualEffectState::kActive; } else if (visual_effect_state == "inactive") { *out = VisualEffectState::kInactive; } else { return false; } return true; } }; } // namespace gin namespace electron { namespace { bool IsFramelessWindow(NSView* view) { NSWindow* nswindow = [view window]; if (![nswindow respondsToSelector:@selector(shell)]) return false; NativeWindow* window = [static_cast(nswindow) shell]; return window && !window->has_frame(); } IMP original_set_frame_size = nullptr; IMP original_view_did_move_to_superview = nullptr; // This method is directly called by NSWindow during a window resize on OSX // 10.10.0, beta 2. We must override it to prevent the content view from // shrinking. void SetFrameSize(NSView* self, SEL _cmd, NSSize size) { if (!IsFramelessWindow(self)) { auto original = reinterpret_cast(original_set_frame_size); return original(self, _cmd, size); } // For frameless window, resize the view to cover full window. if ([self superview]) size = [[self superview] bounds].size; auto super_impl = reinterpret_cast( [[self superclass] instanceMethodForSelector:_cmd]); super_impl(self, _cmd, size); } // The contentView gets moved around during certain full-screen operations. // This is less than ideal, and should eventually be removed. void ViewDidMoveToSuperview(NSView* self, SEL _cmd) { if (!IsFramelessWindow(self)) { // [BridgedContentView viewDidMoveToSuperview]; auto original = reinterpret_cast( original_view_did_move_to_superview); if (original) original(self, _cmd); return; } [self setFrame:[[self superview] bounds]]; } // -[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 ReorderChildWindowAbove(NSWindow* child_window, NSWindow* other_window) { NSWindow* parent = [child_window parentWindow]; DCHECK(parent); // `ordered_children` sorts children windows back to front. NSArray* children = [[child_window parentWindow] childWindows]; std::vector> ordered_children; for (NSWindow* child in children) ordered_children.push_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 // all other children windows. if (other_window == nullptr) other_window = ordered_children.back().second; if (child_window == other_window) return; for (NSWindow* child in children) [parent removeChildWindow:child]; const bool relative_to_parent = parent == other_window; 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) { [parent addChildWindow:other_window ordered:NSWindowAbove]; [parent addChildWindow:child_window ordered:NSWindowAbove]; } } } NSView* GetNativeNSView(NativeBrowserView* view) { if (auto* inspectable = view->GetInspectableWebContentsView()) { return inspectable->GetNativeView().GetNativeNSView(); } else { return nullptr; } } } // namespace NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options, NativeWindow* parent) : NativeWindow(options, parent), root_view_(new RootViewMac(this)) { ui::NativeTheme::GetInstanceForNativeUi()->AddObserver(this); display::Screen::GetScreen()->AddObserver(this); int width = 800, height = 600; options.Get(options::kWidth, &width); options.Get(options::kHeight, &height); NSRect main_screen_rect = [[[NSScreen screens] firstObject] frame]; gfx::Rect bounds(round((NSWidth(main_screen_rect) - width) / 2), round((NSHeight(main_screen_rect) - height) / 2), width, height); bool resizable = true; options.Get(options::kResizable, &resizable); options.Get(options::kZoomToPageWidth, &zoom_to_page_width_); options.Get(options::kSimpleFullScreen, &always_simple_fullscreen_); options.GetOptional(options::kTrafficLightPosition, &traffic_light_position_); options.Get(options::kVisualEffectState, &visual_effect_state_); bool minimizable = true; options.Get(options::kMinimizable, &minimizable); bool maximizable = true; options.Get(options::kMaximizable, &maximizable); bool closable = true; options.Get(options::kClosable, &closable); std::string tabbingIdentifier; options.Get(options::kTabbingIdentifier, &tabbingIdentifier); std::string windowType; options.Get(options::kType, &windowType); bool hiddenInMissionControl = false; options.Get(options::kHiddenInMissionControl, &hiddenInMissionControl); bool useStandardWindow = true; // eventually deprecate separate "standardWindow" option in favor of // standard / textured window types options.Get(options::kStandardWindow, &useStandardWindow); if (windowType == "textured") { useStandardWindow = false; } // The window without titlebar is treated the same with frameless window. if (title_bar_style_ != TitleBarStyle::kNormal) set_has_frame(false); NSUInteger styleMask = NSWindowStyleMaskTitled; // The NSWindowStyleMaskFullSizeContentView style removes rounded corners // for frameless window. bool rounded_corner = true; options.Get(options::kRoundedCorners, &rounded_corner); if (!rounded_corner && !has_frame()) styleMask = NSWindowStyleMaskBorderless; if (minimizable) styleMask |= NSWindowStyleMaskMiniaturizable; if (closable) styleMask |= NSWindowStyleMaskClosable; if (resizable) styleMask |= NSWindowStyleMaskResizable; if (!useStandardWindow || transparent() || !has_frame()) styleMask |= NSWindowStyleMaskTexturedBackground; // Create views::Widget and assign window_ with it. // TODO(zcbenz): Get rid of the window_ in future. views::Widget::InitParams params; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.bounds = bounds; params.delegate = this; params.type = views::Widget::InitParams::TYPE_WINDOW; params.native_widget = new ElectronNativeWidgetMac(this, windowType, styleMask, widget()); widget()->Init(std::move(params)); SetCanResize(resizable); window_ = static_cast( widget()->GetNativeWindow().GetNativeNSWindow()); RegisterDeleteDelegateCallback(base::BindOnce( [](NativeWindowMac* window) { if (window->window_) window->window_ = nil; if (window->buttons_proxy_) window->buttons_proxy_ = nil; }, this)); [window_ setEnableLargerThanScreen:enable_larger_than_screen()]; window_delegate_ = [[ElectronNSWindowDelegate alloc] initWithShell:this]; [window_ setDelegate:window_delegate_]; // Only use native parent window for non-modal windows. if (parent && !is_modal()) { SetParentWindow(parent); } if (transparent()) { // Setting the background color to clear will also hide the shadow. [window_ setBackgroundColor:[NSColor clearColor]]; } if (windowType == "desktop") { [window_ setLevel:kCGDesktopWindowLevel - 1]; [window_ setDisableKeyOrMainWindow:YES]; [window_ setCollectionBehavior:(NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorIgnoresCycle)]; } if (windowType == "panel") { [window_ setLevel:NSFloatingWindowLevel]; } bool focusable; if (options.Get(options::kFocusable, &focusable) && !focusable) [window_ setDisableKeyOrMainWindow:YES]; if (transparent() || !has_frame()) { // Don't show title bar. [window_ setTitlebarAppearsTransparent:YES]; [window_ setTitleVisibility:NSWindowTitleHidden]; // Remove non-transparent corners, see // https://github.com/electron/electron/issues/517. [window_ setOpaque:NO]; // Show window buttons if titleBarStyle is not "normal". if (title_bar_style_ == TitleBarStyle::kNormal) { InternalSetWindowButtonVisibility(false); } else { buttons_proxy_ = [[WindowButtonsProxy alloc] initWithWindow:window_]; [buttons_proxy_ setHeight:titlebar_overlay_height()]; if (traffic_light_position_) { [buttons_proxy_ setMargin:*traffic_light_position_]; } else if (title_bar_style_ == TitleBarStyle::kHiddenInset) { // For macOS >= 11, while this value does not match official macOS apps // like Safari or Notes, it matches titleBarStyle's old implementation // before Electron <= 12. [buttons_proxy_ setMargin:gfx::Point(12, 11)]; } if (title_bar_style_ == TitleBarStyle::kCustomButtonsOnHover) { [buttons_proxy_ setShowOnHover:YES]; } else { // customButtonsOnHover does not show buttons initially. InternalSetWindowButtonVisibility(true); } } } // Create a tab only if tabbing identifier is specified and window has // a native title bar. if (tabbingIdentifier.empty() || transparent() || !has_frame()) { [window_ setTabbingMode:NSWindowTabbingModeDisallowed]; } else { [window_ setTabbingIdentifier:base::SysUTF8ToNSString(tabbingIdentifier)]; } // Resize to content bounds. bool use_content_size = false; options.Get(options::kUseContentSize, &use_content_size); if (!has_frame() || use_content_size) SetContentSize(gfx::Size(width, height)); // Enable the NSView to accept first mouse event. bool acceptsFirstMouse = false; options.Get(options::kAcceptFirstMouse, &acceptsFirstMouse); [window_ setAcceptsFirstMouse:acceptsFirstMouse]; // Disable auto-hiding cursor. bool disableAutoHideCursor = false; options.Get(options::kDisableAutoHideCursor, &disableAutoHideCursor); [window_ setDisableAutoHideCursor:disableAutoHideCursor]; SetHiddenInMissionControl(hiddenInMissionControl); // Set maximizable state last to ensure zoom button does not get reset // by calls to other APIs. SetMaximizable(maximizable); // Default content view. SetContentView(new views::View()); AddContentViewLayers(); UpdateWindowOriginalFrame(); original_level_ = [window_ level]; } NativeWindowMac::~NativeWindowMac() = default; void NativeWindowMac::SetContentView(views::View* view) { views::View* root_view = GetContentsView(); if (content_view()) root_view->RemoveChildView(content_view()); set_content_view(view); root_view->AddChildView(content_view()); root_view->Layout(); } void NativeWindowMac::Close() { if (!IsClosable()) { WindowList::WindowCloseCancelled(this); return; } if (fullscreen_transition_state() != FullScreenTransitionState::kNone) { SetHasDeferredWindowClose(true); return; } // Ensure we're detached from the parent window before closing. RemoveChildFromParentWindow(); while (!child_windows_.empty()) { auto* child = child_windows_.back(); child->RemoveChildFromParentWindow(); } // If a sheet is attached to the window when we call // [window_ performClose:nil], the window won't close properly // even after the user has ended the sheet. // Ensure it's closed before calling [window_ performClose:nil]. if ([window_ attachedSheet]) [window_ endSheet:[window_ attachedSheet]]; [window_ performClose:nil]; // Closing a sheet doesn't trigger windowShouldClose, // so we need to manually call it ourselves here. if (is_modal() && parent() && IsVisible()) { NotifyWindowCloseButtonClicked(); } } void NativeWindowMac::CloseImmediately() { // Ensure we're detached from the parent window before closing. RemoveChildFromParentWindow(); while (!child_windows_.empty()) { auto* child = child_windows_.back(); child->RemoveChildFromParentWindow(); } [window_ close]; } void NativeWindowMac::Focus(bool focus) { if (!IsVisible()) return; if (focus) { [[NSApplication sharedApplication] activateIgnoringOtherApps:NO]; [window_ makeKeyAndOrderFront:nil]; } else { [window_ orderOut:nil]; [window_ orderBack:nil]; } } bool NativeWindowMac::IsFocused() { return [window_ isKeyWindow]; } void NativeWindowMac::Show() { if (is_modal() && parent()) { NSWindow* window = parent()->GetNativeWindow().GetNativeNSWindow(); if ([window_ sheetParent] == nil) [window beginSheet:window_ completionHandler:^(NSModalResponse){ }]; return; } set_wants_to_be_visible(true); // Reattach the window to the parent to actually show it. if (parent()) InternalSetParentWindow(parent(), true); // This method is supposed to put focus on window, however if the app does not // have focus then "makeKeyAndOrderFront" will only show the window. [NSApp activateIgnoringOtherApps:YES]; [window_ makeKeyAndOrderFront:nil]; } void NativeWindowMac::ShowInactive() { // Reattach the window to the parent to actually show it. if (parent()) InternalSetParentWindow(parent(), true); [window_ orderFrontRegardless]; } void NativeWindowMac::Hide() { // If a sheet is attached to the window when we call [window_ orderOut:nil], // the sheet won't be able to show again on the same window. // Ensure it's closed before calling [window_ orderOut:nil]. if ([window_ attachedSheet]) [window_ endSheet:[window_ attachedSheet]]; if (is_modal() && parent()) { [window_ orderOut:nil]; [parent()->GetNativeWindow().GetNativeNSWindow() endSheet:window_]; return; } // If the window wants to be visible and has a parent, then the parent may // order it back in (in the period between orderOut: and close). set_wants_to_be_visible(false); DetachChildren(); // Detach the window from the parent before. if (parent()) InternalSetParentWindow(parent(), false); [window_ orderOut:nil]; } bool NativeWindowMac::IsVisible() { bool occluded = [window_ occlusionState] == NSWindowOcclusionStateVisible; return [window_ isVisible] && !occluded && !IsMinimized(); } bool NativeWindowMac::IsEnabled() { return [window_ attachedSheet] == nil; } void NativeWindowMac::SetEnabled(bool enable) { if (!enable) { NSRect frame = [window_ frame]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, frame.size.width, frame.size.height) styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:NO]; [window setAlphaValue:0.5]; [window_ beginSheet:window completionHandler:^(NSModalResponse returnCode) { NSLog(@"main window disabled"); return; }]; } else if ([window_ attachedSheet]) { [window_ endSheet:[window_ attachedSheet]]; } } void NativeWindowMac::Maximize() { const bool is_visible = [window_ isVisible]; if (IsMaximized()) { if (!is_visible) ShowInactive(); return; } // Take note of the current window size if (IsNormal()) UpdateWindowOriginalFrame(); [window_ zoom:nil]; if (!is_visible) { ShowInactive(); NotifyWindowMaximize(); } } void NativeWindowMac::Unmaximize() { // Bail if the last user set bounds were the same size as the window // screen (e.g. the user set the window to maximized via setBounds) // // Per docs during zoom: // > If there’s no saved user state because there has been no previous // > zoom,the size and location of the window don’t change. // // However, in classic Apple fashion, this is not the case in practice, // and the frame inexplicably becomes very tiny. We should prevent // zoom from being called if the window is being unmaximized and its // unmaximized window bounds are themselves functionally maximized. if (!IsMaximized() || user_set_bounds_maximized_) return; [window_ zoom:nil]; } bool NativeWindowMac::IsMaximized() { // It's possible for [window_ isZoomed] to be true // when the window is minimized or fullscreened. if (IsMinimized() || IsFullscreen()) return false; if (HasStyleMask(NSWindowStyleMaskResizable) != 0) return [window_ isZoomed]; NSRect rectScreen = GetAspectRatio() > 0.0 ? default_frame_for_zoom() : [[NSScreen mainScreen] visibleFrame]; return NSEqualRects([window_ frame], rectScreen); } void NativeWindowMac::Minimize() { if (IsMinimized()) return; // Take note of the current window size if (IsNormal()) UpdateWindowOriginalFrame(); [window_ miniaturize:nil]; } void NativeWindowMac::Restore() { [window_ deminiaturize:nil]; } bool NativeWindowMac::IsMinimized() { return [window_ isMiniaturized]; } bool NativeWindowMac::HandleDeferredClose() { if (has_deferred_window_close_) { SetHasDeferredWindowClose(false); Close(); return true; } return false; } void NativeWindowMac::RemoveChildWindow(NativeWindow* child) { child_windows_.remove_if([&child](NativeWindow* w) { return (w == child); }); [window_ removeChildWindow:child->GetNativeWindow().GetNativeNSWindow()]; } void NativeWindowMac::RemoveChildFromParentWindow() { if (parent() && !is_modal()) { parent()->RemoveChildWindow(this); NativeWindow::SetParentWindow(nullptr); } } void NativeWindowMac::AttachChildren() { for (auto* child : child_windows_) { if (!static_cast(child)->wants_to_be_visible()) continue; auto* child_nswindow = child->GetNativeWindow().GetNativeNSWindow(); if ([child_nswindow parentWindow] == window_) continue; // Attaching a window as a child window resets its window level, so // save and restore it afterwards. NSInteger level = window_.level; [window_ addChildWindow:child_nswindow ordered:NSWindowAbove]; [window_ setLevel:level]; } } void NativeWindowMac::DetachChildren() { // Hide all children before hiding/minimizing the window. // NativeWidgetNSWindowBridge::NotifyVisibilityChangeDown() // will DCHECK otherwise. for (auto* child : child_windows_) { [child->GetNativeWindow().GetNativeNSWindow() orderOut:nil]; } } void NativeWindowMac::SetFullScreen(bool fullscreen) { // [NSWindow -toggleFullScreen] is an asynchronous operation, which means // that it's possible to call it while a fullscreen transition is currently // in process. This can create weird behavior (incl. phantom windows), // so we want to schedule a transition for when the current one has completed. if (fullscreen_transition_state() != FullScreenTransitionState::kNone) { if (!pending_transitions_.empty()) { bool last_pending = pending_transitions_.back(); // Only push new transitions if they're different than the last transition // in the queue. if (last_pending != fullscreen) pending_transitions_.push(fullscreen); } else { pending_transitions_.push(fullscreen); } return; } if (fullscreen == IsFullscreen() || !IsFullScreenable()) return; // Take note of the current window size if (IsNormal()) UpdateWindowOriginalFrame(); // This needs to be set here because it can be the case that // SetFullScreen is called by a user before windowWillEnterFullScreen // or windowWillExitFullScreen are invoked, and so a potential transition // could be dropped. fullscreen_transition_state_ = fullscreen ? FullScreenTransitionState::kEntering : FullScreenTransitionState::kExiting; if (![window_ toggleFullScreenMode:nil]) fullscreen_transition_state_ = FullScreenTransitionState::kNone; } bool NativeWindowMac::IsFullscreen() const { return HasStyleMask(NSWindowStyleMaskFullScreen); } void NativeWindowMac::SetBounds(const gfx::Rect& bounds, bool animate) { // Do nothing if in fullscreen mode. if (IsFullscreen()) return; // Check size constraints since setFrame does not check it. gfx::Size size = bounds.size(); size.SetToMax(GetMinimumSize()); gfx::Size max_size = GetMaximumSize(); if (!max_size.IsEmpty()) size.SetToMin(max_size); NSRect cocoa_bounds = NSMakeRect(bounds.x(), 0, size.width(), size.height()); // Flip coordinates based on the primary screen. NSScreen* screen = [[NSScreen screens] firstObject]; cocoa_bounds.origin.y = NSHeight([screen frame]) - size.height() - bounds.y(); [window_ setFrame:cocoa_bounds display:YES animate:animate]; user_set_bounds_maximized_ = IsMaximized() ? true : false; UpdateWindowOriginalFrame(); } gfx::Rect NativeWindowMac::GetBounds() { NSRect frame = [window_ frame]; gfx::Rect bounds(frame.origin.x, 0, NSWidth(frame), NSHeight(frame)); NSScreen* screen = [[NSScreen screens] firstObject]; bounds.set_y(NSHeight([screen frame]) - NSMaxY(frame)); return bounds; } bool NativeWindowMac::IsNormal() { return NativeWindow::IsNormal() && !IsSimpleFullScreen(); } gfx::Rect NativeWindowMac::GetNormalBounds() { if (IsNormal()) { return GetBounds(); } NSRect frame = original_frame_; gfx::Rect bounds(frame.origin.x, 0, NSWidth(frame), NSHeight(frame)); NSScreen* screen = [[NSScreen screens] firstObject]; bounds.set_y(NSHeight([screen frame]) - NSMaxY(frame)); return bounds; // Works on OS_WIN ! // return widget()->GetRestoredBounds(); } void NativeWindowMac::SetSizeConstraints( const extensions::SizeConstraints& window_constraints) { // Apply the size constraints to NSWindow. if (window_constraints.HasMinimumSize()) [window_ setMinSize:window_constraints.GetMinimumSize().ToCGSize()]; if (window_constraints.HasMaximumSize()) [window_ setMaxSize:window_constraints.GetMaximumSize().ToCGSize()]; NativeWindow::SetSizeConstraints(window_constraints); } void NativeWindowMac::SetContentSizeConstraints( const extensions::SizeConstraints& size_constraints) { auto convertSize = [this](const gfx::Size& size) { // Our frameless window still has titlebar attached, so setting contentSize // will result in actual content size being larger. if (!has_frame()) { NSRect frame = NSMakeRect(0, 0, size.width(), size.height()); NSRect content = [window_ originalContentRectForFrameRect:frame]; return content.size; } else { return NSMakeSize(size.width(), size.height()); } }; // Apply the size constraints to NSWindow. NSView* content = [window_ contentView]; if (size_constraints.HasMinimumSize()) { NSSize min_size = convertSize(size_constraints.GetMinimumSize()); [window_ setContentMinSize:[content convertSize:min_size toView:nil]]; } if (size_constraints.HasMaximumSize()) { NSSize max_size = convertSize(size_constraints.GetMaximumSize()); [window_ setContentMaxSize:[content convertSize:max_size toView:nil]]; } NativeWindow::SetContentSizeConstraints(size_constraints); } bool NativeWindowMac::MoveAbove(const std::string& sourceId) { const content::DesktopMediaID id = content::DesktopMediaID::Parse(sourceId); if (id.type != content::DesktopMediaID::TYPE_WINDOW) return false; // Check if the window source is valid. const CGWindowID window_id = id.id; if (!webrtc::GetWindowOwnerPid(window_id)) return false; if (!parent() || is_modal()) { [window_ orderWindow:NSWindowAbove relativeTo:window_id]; } else { NSWindow* other_window = [NSApp windowWithWindowNumber:window_id]; ReorderChildWindowAbove(window_, other_window); } return true; } void NativeWindowMac::MoveTop() { if (!parent() || is_modal()) { [window_ orderWindow:NSWindowAbove relativeTo:0]; } else { ReorderChildWindowAbove(window_, nullptr); } } void NativeWindowMac::SetResizable(bool resizable) { ScopedDisableResize disable_resize; SetStyleMask(resizable, NSWindowStyleMaskResizable); bool was_fullscreenable = IsFullScreenable(); // Right now, resizable and fullscreenable are decoupled in // documentation and on Windows/Linux. Chromium disables // fullscreen collection behavior as well as the maximize traffic // light in SetCanResize if resizability is false on macOS unless // the window is both resizable and maximizable. We want consistent // cross-platform behavior, so if resizability is disabled we disable // the maximize button and ensure fullscreenability matches user setting. SetCanResize(resizable); SetFullScreenable(was_fullscreenable); [[window_ standardWindowButton:NSWindowZoomButton] setEnabled:resizable ? was_fullscreenable : false]; } bool NativeWindowMac::IsResizable() { bool in_fs_transition = fullscreen_transition_state() != FullScreenTransitionState::kNone; bool has_rs_mask = HasStyleMask(NSWindowStyleMaskResizable); return has_rs_mask && !IsFullscreen() && !in_fs_transition; } void NativeWindowMac::SetMovable(bool movable) { [window_ setMovable:movable]; } bool NativeWindowMac::IsMovable() { return [window_ isMovable]; } void NativeWindowMac::SetMinimizable(bool minimizable) { SetStyleMask(minimizable, NSWindowStyleMaskMiniaturizable); } bool NativeWindowMac::IsMinimizable() { return HasStyleMask(NSWindowStyleMaskMiniaturizable); } void NativeWindowMac::SetMaximizable(bool maximizable) { maximizable_ = maximizable; [[window_ standardWindowButton:NSWindowZoomButton] setEnabled:maximizable]; } bool NativeWindowMac::IsMaximizable() { return [[window_ standardWindowButton:NSWindowZoomButton] isEnabled]; } void NativeWindowMac::SetFullScreenable(bool fullscreenable) { SetCollectionBehavior(fullscreenable, NSWindowCollectionBehaviorFullScreenPrimary); // On EL Capitan this flag is required to hide fullscreen button. SetCollectionBehavior(!fullscreenable, NSWindowCollectionBehaviorFullScreenAuxiliary); } bool NativeWindowMac::IsFullScreenable() { NSUInteger collectionBehavior = [window_ collectionBehavior]; return collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary; } void NativeWindowMac::SetClosable(bool closable) { SetStyleMask(closable, NSWindowStyleMaskClosable); } bool NativeWindowMac::IsClosable() { return HasStyleMask(NSWindowStyleMaskClosable); } void NativeWindowMac::SetAlwaysOnTop(ui::ZOrderLevel z_order, const std::string& level_name, int relative_level) { if (z_order == ui::ZOrderLevel::kNormal) { SetWindowLevel(NSNormalWindowLevel); return; } int level = NSNormalWindowLevel; if (level_name == "floating") { level = NSFloatingWindowLevel; } else if (level_name == "torn-off-menu") { level = NSTornOffMenuWindowLevel; } else if (level_name == "modal-panel") { level = NSModalPanelWindowLevel; } else if (level_name == "main-menu") { level = NSMainMenuWindowLevel; } else if (level_name == "status") { level = NSStatusWindowLevel; } else if (level_name == "pop-up-menu") { level = NSPopUpMenuWindowLevel; } else if (level_name == "screen-saver") { level = NSScreenSaverWindowLevel; } SetWindowLevel(level + relative_level); } std::string NativeWindowMac::GetAlwaysOnTopLevel() { std::string level_name = "normal"; int level = [window_ level]; if (level == NSFloatingWindowLevel) { level_name = "floating"; } else if (level == NSTornOffMenuWindowLevel) { level_name = "torn-off-menu"; } else if (level == NSModalPanelWindowLevel) { level_name = "modal-panel"; } else if (level == NSMainMenuWindowLevel) { level_name = "main-menu"; } else if (level == NSStatusWindowLevel) { level_name = "status"; } else if (level == NSPopUpMenuWindowLevel) { level_name = "pop-up-menu"; } else if (level == NSScreenSaverWindowLevel) { level_name = "screen-saver"; } return level_name; } void NativeWindowMac::SetWindowLevel(int unbounded_level) { int level = std::min( std::max(unbounded_level, CGWindowLevelForKey(kCGMinimumWindowLevelKey)), CGWindowLevelForKey(kCGMaximumWindowLevelKey)); ui::ZOrderLevel z_order_level = level == NSNormalWindowLevel ? ui::ZOrderLevel::kNormal : ui::ZOrderLevel::kFloatingWindow; bool did_z_order_level_change = z_order_level != GetZOrderLevel(); was_maximizable_ = IsMaximizable(); // We need to explicitly keep the NativeWidget up to date, since it stores the // window level in a local variable, rather than reading it from the NSWindow. // Unfortunately, it results in a second setLevel call. It's not ideal, but we // don't expect this to cause any user-visible jank. widget()->SetZOrderLevel(z_order_level); [window_ setLevel:level]; // Set level will make the zoom button revert to default, probably // a bug of Cocoa or macOS. SetMaximizable(was_maximizable_); // This must be notified at the very end or IsAlwaysOnTop // will not yet have been updated to reflect the new status if (did_z_order_level_change) NativeWindow::NotifyWindowAlwaysOnTopChanged(); } ui::ZOrderLevel NativeWindowMac::GetZOrderLevel() { return widget()->GetZOrderLevel(); } void NativeWindowMac::Center() { [window_ center]; } void NativeWindowMac::Invalidate() { [[window_ contentView] setNeedsDisplay:YES]; } void NativeWindowMac::SetTitle(const std::string& title) { [window_ setTitle:base::SysUTF8ToNSString(title)]; if (buttons_proxy_) [buttons_proxy_ redraw]; } std::string NativeWindowMac::GetTitle() { return base::SysNSStringToUTF8([window_ title]); } void NativeWindowMac::FlashFrame(bool flash) { if (flash) { attention_request_id_ = [NSApp requestUserAttention:NSInformationalRequest]; } else { [NSApp cancelUserAttentionRequest:attention_request_id_]; attention_request_id_ = 0; } } void NativeWindowMac::SetSkipTaskbar(bool skip) {} bool NativeWindowMac::IsExcludedFromShownWindowsMenu() { NSWindow* window = GetNativeWindow().GetNativeNSWindow(); return [window isExcludedFromWindowsMenu]; } void NativeWindowMac::SetExcludedFromShownWindowsMenu(bool excluded) { NSWindow* window = GetNativeWindow().GetNativeNSWindow(); [window setExcludedFromWindowsMenu:excluded]; } void NativeWindowMac::OnDisplayMetricsChanged(const display::Display& display, uint32_t changed_metrics) { // We only want to force screen recalibration if we're in simpleFullscreen // mode. if (!is_simple_fullscreen_) return; content::GetUIThreadTaskRunner({})->PostTask( FROM_HERE, base::BindOnce(&NativeWindow::UpdateFrame, GetWeakPtr())); } void NativeWindowMac::SetSimpleFullScreen(bool simple_fullscreen) { NSWindow* window = GetNativeWindow().GetNativeNSWindow(); if (simple_fullscreen && !is_simple_fullscreen_) { is_simple_fullscreen_ = true; // Take note of the current window size and level if (IsNormal()) { UpdateWindowOriginalFrame(); original_level_ = [window_ level]; } simple_fullscreen_options_ = [NSApp currentSystemPresentationOptions]; simple_fullscreen_mask_ = [window styleMask]; // We can simulate the pre-Lion fullscreen by auto-hiding the dock and menu // bar NSApplicationPresentationOptions options = NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar; [NSApp setPresentationOptions:options]; was_maximizable_ = IsMaximizable(); was_movable_ = IsMovable(); NSRect fullscreenFrame = [window.screen frame]; // If our app has dock hidden, set the window level higher so another app's // menu bar doesn't appear on top of our fullscreen app. if ([[NSRunningApplication currentApplication] activationPolicy] != NSApplicationActivationPolicyRegular) { window.level = NSPopUpMenuWindowLevel; } // Always hide the titlebar in simple fullscreen mode. // // Note that we must remove the NSWindowStyleMaskTitled style instead of // using the [window_ setTitleVisibility:], as the latter would leave the // window with rounded corners. SetStyleMask(false, NSWindowStyleMaskTitled); if (!window_button_visibility_.has_value()) { // Lets keep previous behaviour - hide window controls in titled // fullscreen mode when not specified otherwise. InternalSetWindowButtonVisibility(false); } [window setFrame:fullscreenFrame display:YES animate:YES]; // Fullscreen windows can't be resized, minimized, maximized, or moved SetMinimizable(false); SetResizable(false); SetMaximizable(false); SetMovable(false); } else if (!simple_fullscreen && is_simple_fullscreen_) { is_simple_fullscreen_ = false; [window setFrame:original_frame_ display:YES animate:YES]; window.level = original_level_; [NSApp setPresentationOptions:simple_fullscreen_options_]; // Restore original style mask ScopedDisableResize disable_resize; [window_ setStyleMask:simple_fullscreen_mask_]; // Restore window manipulation abilities SetMaximizable(was_maximizable_); SetMovable(was_movable_); // Restore default window controls visibility state. if (!window_button_visibility_.has_value()) { bool visibility; if (has_frame()) visibility = true; else visibility = title_bar_style_ != TitleBarStyle::kNormal; InternalSetWindowButtonVisibility(visibility); } if (buttons_proxy_) [buttons_proxy_ redraw]; } } bool NativeWindowMac::IsSimpleFullScreen() { return is_simple_fullscreen_; } void NativeWindowMac::SetKiosk(bool kiosk) { if (kiosk && !is_kiosk_) { kiosk_options_ = [NSApp currentSystemPresentationOptions]; NSApplicationPresentationOptions options = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar | NSApplicationPresentationDisableAppleMenu | NSApplicationPresentationDisableProcessSwitching | NSApplicationPresentationDisableForceQuit | NSApplicationPresentationDisableSessionTermination | NSApplicationPresentationDisableHideApplication; [NSApp setPresentationOptions:options]; is_kiosk_ = true; fullscreen_before_kiosk_ = IsFullscreen(); if (!fullscreen_before_kiosk_) SetFullScreen(true); } else if (!kiosk && is_kiosk_) { is_kiosk_ = false; if (!fullscreen_before_kiosk_) SetFullScreen(false); // Set presentation options *after* asynchronously exiting // fullscreen to ensure they take effect. [NSApp setPresentationOptions:kiosk_options_]; } } bool NativeWindowMac::IsKiosk() { return is_kiosk_; } void NativeWindowMac::SetBackgroundColor(SkColor color) { base::apple::ScopedCFTypeRef cgcolor( skia::CGColorCreateFromSkColor(color)); [[[window_ contentView] layer] setBackgroundColor:cgcolor]; } SkColor NativeWindowMac::GetBackgroundColor() { CGColorRef color = [[[window_ contentView] layer] backgroundColor]; if (!color) return SK_ColorTRANSPARENT; return skia::CGColorRefToSkColor(color); } void NativeWindowMac::SetHasShadow(bool has_shadow) { [window_ setHasShadow:has_shadow]; } bool NativeWindowMac::HasShadow() { return [window_ hasShadow]; } void NativeWindowMac::InvalidateShadow() { [window_ invalidateShadow]; } void NativeWindowMac::SetOpacity(const double opacity) { const double boundedOpacity = std::clamp(opacity, 0.0, 1.0); [window_ setAlphaValue:boundedOpacity]; } double NativeWindowMac::GetOpacity() { return [window_ alphaValue]; } void NativeWindowMac::SetRepresentedFilename(const std::string& filename) { [window_ setRepresentedFilename:base::SysUTF8ToNSString(filename)]; if (buttons_proxy_) [buttons_proxy_ redraw]; } std::string NativeWindowMac::GetRepresentedFilename() { return base::SysNSStringToUTF8([window_ representedFilename]); } void NativeWindowMac::SetDocumentEdited(bool edited) { [window_ setDocumentEdited:edited]; if (buttons_proxy_) [buttons_proxy_ redraw]; } bool NativeWindowMac::IsDocumentEdited() { return [window_ isDocumentEdited]; } bool NativeWindowMac::IsHiddenInMissionControl() { NSUInteger collectionBehavior = [window_ collectionBehavior]; return collectionBehavior & NSWindowCollectionBehaviorTransient; } void NativeWindowMac::SetHiddenInMissionControl(bool hidden) { SetCollectionBehavior(hidden, NSWindowCollectionBehaviorTransient); } void NativeWindowMac::SetIgnoreMouseEvents(bool ignore, bool forward) { [window_ setIgnoresMouseEvents:ignore]; if (!ignore) { SetForwardMouseMessages(NO); } else { SetForwardMouseMessages(forward); } } void NativeWindowMac::SetContentProtection(bool enable) { [window_ setSharingType:enable ? NSWindowSharingNone : NSWindowSharingReadOnly]; } void NativeWindowMac::SetFocusable(bool focusable) { // No known way to unfocus the window if it had the focus. Here we do not // want to call Focus(false) because it moves the window to the back, i.e. // at the bottom in term of z-order. [window_ setDisableKeyOrMainWindow:!focusable]; } bool NativeWindowMac::IsFocusable() { return ![window_ disableKeyOrMainWindow]; } void NativeWindowMac::AddBrowserView(NativeBrowserView* view) { [CATransaction begin]; [CATransaction setDisableActions:YES]; if (!view) { [CATransaction commit]; return; } add_browser_view(view); if (auto* native_view = GetNativeNSView(view)) { [[window_ contentView] addSubview:native_view positioned:NSWindowAbove relativeTo:nil]; native_view.hidden = NO; } [CATransaction commit]; } void NativeWindowMac::RemoveBrowserView(NativeBrowserView* view) { [CATransaction begin]; [CATransaction setDisableActions:YES]; if (!view) { [CATransaction commit]; return; } if (auto* native_view = GetNativeNSView(view)) { [native_view removeFromSuperview]; } remove_browser_view(view); [CATransaction commit]; } void NativeWindowMac::SetTopBrowserView(NativeBrowserView* view) { [CATransaction begin]; [CATransaction setDisableActions:YES]; if (!view) { [CATransaction commit]; return; } remove_browser_view(view); add_browser_view(view); if (auto* native_view = GetNativeNSView(view)) { [[window_ contentView] addSubview:native_view positioned:NSWindowAbove relativeTo:nil]; native_view.hidden = NO; } [CATransaction commit]; } void NativeWindowMac::SetParentWindow(NativeWindow* parent) { InternalSetParentWindow(parent, IsVisible()); } gfx::NativeView NativeWindowMac::GetNativeView() const { return [window_ contentView]; } gfx::NativeWindow NativeWindowMac::GetNativeWindow() const { return window_; } gfx::AcceleratedWidget NativeWindowMac::GetAcceleratedWidget() const { return [window_ windowNumber]; } content::DesktopMediaID NativeWindowMac::GetDesktopMediaID() const { auto desktop_media_id = content::DesktopMediaID( content::DesktopMediaID::TYPE_WINDOW, GetAcceleratedWidget()); // c.f. // https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/media/webrtc/native_desktop_media_list.cc;l=775-780;drc=79502ab47f61bff351426f57f576daef02b1a8dc // Refs https://github.com/electron/electron/pull/30507 // TODO(deepak1556): Match upstream for `kWindowCaptureMacV2` #if 0 if (remote_cocoa::ScopedCGWindowID::Get(desktop_media_id.id)) { desktop_media_id.window_id = desktop_media_id.id; } #endif return desktop_media_id; } NativeWindowHandle NativeWindowMac::GetNativeWindowHandle() const { return [window_ contentView]; } void NativeWindowMac::SetProgressBar(double progress, const NativeWindow::ProgressState state) { NSDockTile* dock_tile = [NSApp dockTile]; // Sometimes macOS would install a default contentView for dock, we must // verify whether NSProgressIndicator has been installed. bool first_time = !dock_tile.contentView || [[dock_tile.contentView subviews] count] == 0 || ![[[dock_tile.contentView subviews] lastObject] isKindOfClass:[NSProgressIndicator class]]; // For the first time API invoked, we need to create a ContentView in // DockTile. if (first_time) { NSImageView* image_view = [[NSImageView alloc] init]; [image_view setImage:[NSApp applicationIconImage]]; [dock_tile setContentView:image_view]; NSRect frame = NSMakeRect(0.0f, 0.0f, dock_tile.size.width, 15.0); NSProgressIndicator* progress_indicator = [[ElectronProgressBar alloc] initWithFrame:frame]; [progress_indicator setStyle:NSProgressIndicatorStyleBar]; [progress_indicator setIndeterminate:NO]; [progress_indicator setBezeled:YES]; [progress_indicator setMinValue:0]; [progress_indicator setMaxValue:1]; [progress_indicator setHidden:NO]; [dock_tile.contentView addSubview:progress_indicator]; } NSProgressIndicator* progress_indicator = static_cast( [[[dock_tile contentView] subviews] lastObject]); if (progress < 0) { [progress_indicator setHidden:YES]; } else if (progress > 1) { [progress_indicator setHidden:NO]; [progress_indicator setIndeterminate:YES]; [progress_indicator setDoubleValue:1]; } else { [progress_indicator setHidden:NO]; [progress_indicator setDoubleValue:progress]; } [dock_tile display]; } void NativeWindowMac::SetOverlayIcon(const gfx::Image& overlay, const std::string& description) {} void NativeWindowMac::SetVisibleOnAllWorkspaces(bool visible, bool visibleOnFullScreen, bool skipTransformProcessType) { // In order for NSWindows to be visible on fullscreen we need to invoke // app.dock.hide() since Apple changed the underlying functionality of // NSWindows starting with 10.14 to disallow NSWindows from floating on top of // fullscreen apps. if (!skipTransformProcessType) { if (visibleOnFullScreen) { Browser::Get()->DockHide(); } else { Browser::Get()->DockShow(JavascriptEnvironment::GetIsolate()); } } SetCollectionBehavior(visible, NSWindowCollectionBehaviorCanJoinAllSpaces); SetCollectionBehavior(visibleOnFullScreen, NSWindowCollectionBehaviorFullScreenAuxiliary); } bool NativeWindowMac::IsVisibleOnAllWorkspaces() { NSUInteger collectionBehavior = [window_ collectionBehavior]; return collectionBehavior & NSWindowCollectionBehaviorCanJoinAllSpaces; } void NativeWindowMac::SetAutoHideCursor(bool auto_hide) { [window_ setDisableAutoHideCursor:!auto_hide]; } void NativeWindowMac::UpdateVibrancyRadii(bool fullscreen) { NSVisualEffectView* vibrantView = [window_ vibrantView]; if (vibrantView != nil && !vibrancy_type_.empty()) { const bool no_rounded_corner = !HasStyleMask(NSWindowStyleMaskTitled); const int macos_version = base::mac::MacOSMajorVersion(); // Modal window corners are rounded on macOS >= 11 or higher if the user // hasn't passed noRoundedCorners. bool should_round_modal = !no_rounded_corner && (macos_version >= 11 ? true : !is_modal()); // Nonmodal window corners are rounded if they're frameless and the user // hasn't passed noRoundedCorners. bool should_round_nonmodal = !no_rounded_corner && !has_frame(); if (should_round_nonmodal || should_round_modal) { CGFloat radius; if (fullscreen) { radius = 0.0f; } else if (macos_version >= 11) { radius = 9.0f; } else { // Smaller corner radius on versions prior to Big Sur. radius = 5.0f; } CGFloat dimension = 2 * radius + 1; NSSize size = NSMakeSize(dimension, dimension); NSImage* maskImage = [NSImage imageWithSize:size flipped:NO drawingHandler:^BOOL(NSRect rect) { NSBezierPath* bezierPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; [[NSColor blackColor] set]; [bezierPath fill]; return YES; }]; [maskImage setCapInsets:NSEdgeInsetsMake(radius, radius, radius, radius)]; [maskImage setResizingMode:NSImageResizingModeStretch]; [vibrantView setMaskImage:maskImage]; [window_ setCornerMask:maskImage]; } } } void NativeWindowMac::UpdateWindowOriginalFrame() { original_frame_ = [window_ frame]; } void NativeWindowMac::SetVibrancy(const std::string& type) { NativeWindow::SetVibrancy(type); NSVisualEffectView* vibrantView = [window_ vibrantView]; if (type.empty()) { if (vibrantView == nil) return; [vibrantView removeFromSuperview]; [window_ setVibrantView:nil]; return; } NSVisualEffectMaterial vibrancyType{}; if (type == "titlebar") { vibrancyType = NSVisualEffectMaterialTitlebar; } else if (type == "selection") { vibrancyType = NSVisualEffectMaterialSelection; } else if (type == "menu") { vibrancyType = NSVisualEffectMaterialMenu; } else if (type == "popover") { vibrancyType = NSVisualEffectMaterialPopover; } else if (type == "sidebar") { vibrancyType = NSVisualEffectMaterialSidebar; } else if (type == "header") { vibrancyType = NSVisualEffectMaterialHeaderView; } else if (type == "sheet") { vibrancyType = NSVisualEffectMaterialSheet; } else if (type == "window") { vibrancyType = NSVisualEffectMaterialWindowBackground; } else if (type == "hud") { vibrancyType = NSVisualEffectMaterialHUDWindow; } else if (type == "fullscreen-ui") { vibrancyType = NSVisualEffectMaterialFullScreenUI; } else if (type == "tooltip") { vibrancyType = NSVisualEffectMaterialToolTip; } else if (type == "content") { vibrancyType = NSVisualEffectMaterialContentBackground; } else if (type == "under-window") { vibrancyType = NSVisualEffectMaterialUnderWindowBackground; } else if (type == "under-page") { vibrancyType = NSVisualEffectMaterialUnderPageBackground; } if (vibrancyType) { vibrancy_type_ = type; if (vibrantView == nil) { vibrantView = [[NSVisualEffectView alloc] initWithFrame:[[window_ contentView] bounds]]; [window_ setVibrantView:vibrantView]; [vibrantView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [vibrantView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; if (visual_effect_state_ == VisualEffectState::kActive) { [vibrantView setState:NSVisualEffectStateActive]; } else if (visual_effect_state_ == VisualEffectState::kInactive) { [vibrantView setState:NSVisualEffectStateInactive]; } else { [vibrantView setState:NSVisualEffectStateFollowsWindowActiveState]; } [[window_ contentView] addSubview:vibrantView positioned:NSWindowBelow relativeTo:nil]; UpdateVibrancyRadii(IsFullscreen()); } [vibrantView setMaterial:vibrancyType]; } } void NativeWindowMac::SetWindowButtonVisibility(bool visible) { window_button_visibility_ = visible; if (buttons_proxy_) { if (visible) [buttons_proxy_ redraw]; [buttons_proxy_ setVisible:visible]; } if (title_bar_style_ != TitleBarStyle::kCustomButtonsOnHover) InternalSetWindowButtonVisibility(visible); NotifyLayoutWindowControlsOverlay(); } bool NativeWindowMac::GetWindowButtonVisibility() const { return ![window_ standardWindowButton:NSWindowZoomButton].hidden || ![window_ standardWindowButton:NSWindowMiniaturizeButton].hidden || ![window_ standardWindowButton:NSWindowCloseButton].hidden; } void NativeWindowMac::SetWindowButtonPosition( absl::optional position) { traffic_light_position_ = std::move(position); if (buttons_proxy_) { [buttons_proxy_ setMargin:traffic_light_position_]; NotifyLayoutWindowControlsOverlay(); } } absl::optional NativeWindowMac::GetWindowButtonPosition() const { return traffic_light_position_; } void NativeWindowMac::RedrawTrafficLights() { if (buttons_proxy_ && !IsFullscreen()) [buttons_proxy_ redraw]; } // In simpleFullScreen mode, update the frame for new bounds. void NativeWindowMac::UpdateFrame() { NSWindow* window = GetNativeWindow().GetNativeNSWindow(); NSRect fullscreenFrame = [window.screen frame]; [window setFrame:fullscreenFrame display:YES animate:YES]; } void NativeWindowMac::SetTouchBar( std::vector items) { touch_bar_ = [[ElectronTouchBar alloc] initWithDelegate:window_delegate_ window:this settings:std::move(items)]; [window_ setTouchBar:nil]; } void NativeWindowMac::RefreshTouchBarItem(const std::string& item_id) { if (touch_bar_ && [window_ touchBar]) [touch_bar_ refreshTouchBarItem:[window_ touchBar] id:item_id]; } void NativeWindowMac::SetEscapeTouchBarItem( gin_helper::PersistentDictionary item) { if (touch_bar_ && [window_ touchBar]) [touch_bar_ setEscapeTouchBarItem:std::move(item) forTouchBar:[window_ touchBar]]; } void NativeWindowMac::SelectPreviousTab() { [window_ selectPreviousTab:nil]; } void NativeWindowMac::SelectNextTab() { [window_ selectNextTab:nil]; } void NativeWindowMac::ShowAllTabs() { [window_ toggleTabOverview:nil]; } void NativeWindowMac::MergeAllWindows() { [window_ mergeAllWindows:nil]; } void NativeWindowMac::MoveTabToNewWindow() { [window_ moveTabToNewWindow:nil]; } void NativeWindowMac::ToggleTabBar() { [window_ toggleTabBar:nil]; } bool NativeWindowMac::AddTabbedWindow(NativeWindow* window) { if (window_ == window->GetNativeWindow().GetNativeNSWindow()) { return false; } else { [window_ addTabbedWindow:window->GetNativeWindow().GetNativeNSWindow() ordered:NSWindowAbove]; } return true; } absl::optional NativeWindowMac::GetTabbingIdentifier() const { if ([window_ tabbingMode] == NSWindowTabbingModeDisallowed) return absl::nullopt; return base::SysNSStringToUTF8([window_ tabbingIdentifier]); } void NativeWindowMac::SetAspectRatio(double aspect_ratio, const gfx::Size& extra_size) { NativeWindow::SetAspectRatio(aspect_ratio, extra_size); // Reset the behaviour to default if aspect_ratio is set to 0 or less. if (aspect_ratio > 0.0) { NSSize aspect_ratio_size = NSMakeSize(aspect_ratio, 1.0); if (has_frame()) [window_ setContentAspectRatio:aspect_ratio_size]; else [window_ setAspectRatio:aspect_ratio_size]; } else { [window_ setResizeIncrements:NSMakeSize(1.0, 1.0)]; } } void NativeWindowMac::PreviewFile(const std::string& path, const std::string& display_name) { preview_item_ = [[ElectronPreviewItem alloc] initWithURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString(path)] title:base::SysUTF8ToNSString(display_name)]; [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil]; } void NativeWindowMac::CloseFilePreview() { if ([QLPreviewPanel sharedPreviewPanelExists]) { [[QLPreviewPanel sharedPreviewPanel] close]; } } gfx::Rect NativeWindowMac::ContentBoundsToWindowBounds( const gfx::Rect& bounds) const { if (has_frame()) { gfx::Rect window_bounds( [window_ frameRectForContentRect:bounds.ToCGRect()]); int frame_height = window_bounds.height() - bounds.height(); window_bounds.set_y(window_bounds.y() - frame_height); return window_bounds; } else { return bounds; } } gfx::Rect NativeWindowMac::WindowBoundsToContentBounds( const gfx::Rect& bounds) const { if (has_frame()) { gfx::Rect content_bounds( [window_ contentRectForFrameRect:bounds.ToCGRect()]); int frame_height = bounds.height() - content_bounds.height(); content_bounds.set_y(content_bounds.y() + frame_height); return content_bounds; } else { return bounds; } } void NativeWindowMac::NotifyWindowEnterFullScreen() { NativeWindow::NotifyWindowEnterFullScreen(); // Restore the window title under fullscreen mode. if (buttons_proxy_) [window_ setTitleVisibility:NSWindowTitleVisible]; if (transparent() || !has_frame()) [window_ setTitlebarAppearsTransparent:NO]; } void NativeWindowMac::NotifyWindowLeaveFullScreen() { NativeWindow::NotifyWindowLeaveFullScreen(); // Restore window buttons. if (buttons_proxy_ && window_button_visibility_.value_or(true)) { [buttons_proxy_ redraw]; [buttons_proxy_ setVisible:YES]; } if (transparent() || !has_frame()) [window_ setTitlebarAppearsTransparent:YES]; } void NativeWindowMac::NotifyWindowWillEnterFullScreen() { UpdateVibrancyRadii(true); } void NativeWindowMac::NotifyWindowWillLeaveFullScreen() { if (buttons_proxy_) { // Hide window title when leaving fullscreen. [window_ setTitleVisibility:NSWindowTitleHidden]; // Hide the container otherwise traffic light buttons jump. [buttons_proxy_ setVisible:NO]; } UpdateVibrancyRadii(false); } void NativeWindowMac::SetActive(bool is_key) { is_active_ = is_key; } bool NativeWindowMac::IsActive() const { return is_active_; } void NativeWindowMac::Cleanup() { DCHECK(!IsClosed()); ui::NativeTheme::GetInstanceForNativeUi()->RemoveObserver(this); display::Screen::GetScreen()->RemoveObserver(this); } class NativeAppWindowFrameViewMac : public views::NativeFrameViewMac { public: NativeAppWindowFrameViewMac(views::Widget* frame, NativeWindowMac* window) : views::NativeFrameViewMac(frame), native_window_(window) {} NativeAppWindowFrameViewMac(const NativeAppWindowFrameViewMac&) = delete; NativeAppWindowFrameViewMac& operator=(const NativeAppWindowFrameViewMac&) = delete; ~NativeAppWindowFrameViewMac() override = default; // NonClientFrameView: int NonClientHitTest(const gfx::Point& point) override { if (!bounds().Contains(point)) return HTNOWHERE; if (GetWidget()->IsFullscreen()) return HTCLIENT; // Check for possible draggable region in the client area for the frameless // window. int contents_hit_test = native_window_->NonClientHitTest(point); if (contents_hit_test != HTNOWHERE) return contents_hit_test; return HTCLIENT; } private: // Weak. raw_ptr const native_window_; }; std::unique_ptr NativeWindowMac::CreateNonClientFrameView(views::Widget* widget) { return std::make_unique(widget, this); } bool NativeWindowMac::HasStyleMask(NSUInteger flag) const { return [window_ styleMask] & flag; } void NativeWindowMac::SetStyleMask(bool on, NSUInteger flag) { // Changing the styleMask of a frameless windows causes it to change size so // we explicitly disable resizing while setting it. ScopedDisableResize disable_resize; if (on) [window_ setStyleMask:[window_ styleMask] | flag]; else [window_ setStyleMask:[window_ styleMask] & (~flag)]; // Change style mask will make the zoom button revert to default, probably // a bug of Cocoa or macOS. SetMaximizable(maximizable_); } void NativeWindowMac::SetCollectionBehavior(bool on, NSUInteger flag) { if (on) [window_ setCollectionBehavior:[window_ collectionBehavior] | flag]; else [window_ setCollectionBehavior:[window_ collectionBehavior] & (~flag)]; // Change collectionBehavior will make the zoom button revert to default, // probably a bug of Cocoa or macOS. SetMaximizable(maximizable_); } views::View* NativeWindowMac::GetContentsView() { return root_view_.get(); } bool NativeWindowMac::CanMaximize() const { return maximizable_; } void NativeWindowMac::OnNativeThemeUpdated(ui::NativeTheme* observed_theme) { content::GetUIThreadTaskRunner({})->PostTask( FROM_HERE, base::BindOnce(&NativeWindow::RedrawTrafficLights, GetWeakPtr())); } void NativeWindowMac::AddContentViewLayers() { // Make sure the bottom corner is rounded for non-modal windows: // http://crbug.com/396264. if (!is_modal()) { // For normal window, we need to explicitly set layer for contentView to // make setBackgroundColor work correctly. // There is no need to do so for frameless window, and doing so would make // titleBarStyle stop working. if (has_frame()) { CALayer* background_layer = [[CALayer alloc] init]; [background_layer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; [[window_ contentView] setLayer:background_layer]; } [[window_ contentView] setWantsLayer:YES]; } } void NativeWindowMac::InternalSetWindowButtonVisibility(bool visible) { [[window_ standardWindowButton:NSWindowCloseButton] setHidden:!visible]; [[window_ standardWindowButton:NSWindowMiniaturizeButton] setHidden:!visible]; [[window_ standardWindowButton:NSWindowZoomButton] setHidden:!visible]; } void NativeWindowMac::InternalSetParentWindow(NativeWindow* new_parent, bool attach) { if (is_modal()) return; // Do not remove/add if we are already properly attached. if (attach && new_parent && [window_ parentWindow] == new_parent->GetNativeWindow().GetNativeNSWindow()) return; // Remove current parent window. RemoveChildFromParentWindow(); // Set new parent window. if (new_parent) { new_parent->add_child_window(this); if (attach) new_parent->AttachChildren(); } NativeWindow::SetParentWindow(new_parent); } void NativeWindowMac::SetForwardMouseMessages(bool forward) { [window_ setAcceptsMouseMovedEvents:forward]; } absl::optional NativeWindowMac::GetWindowControlsOverlayRect() { if (!titlebar_overlay_) return absl::nullopt; // On macOS, when in fullscreen mode, window controls (the menu bar, title // bar, and toolbar) are attached to a separate NSView that slides down from // the top of the screen, independent of, and overlapping the WebContents. // Disable WCO when in fullscreen, because this space is inaccessible to // WebContents. https://crbug.com/915110. if (IsFullscreen()) return gfx::Rect(); if (buttons_proxy_ && window_button_visibility_.value_or(true)) { NSRect buttons = [buttons_proxy_ getButtonsContainerBounds]; gfx::Rect overlay; overlay.set_width(GetContentSize().width() - NSWidth(buttons)); overlay.set_height([buttons_proxy_ useCustomHeight] ? titlebar_overlay_height() : NSHeight(buttons)); if (!base::i18n::IsRTL()) overlay.set_x(NSMaxX(buttons)); return overlay; } return absl::nullopt; } // static NativeWindow* NativeWindow::Create(const gin_helper::Dictionary& options, NativeWindow* parent) { return new NativeWindowMac(options, parent); } } // namespace electron