electron/shell/browser/native_window_mac.mm

1969 строки
64 KiB
Plaintext
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

// 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 <AvailabilityMacros.h>
#include <objc/objc-runtime.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#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<electron::NativeWindowMac::VisualEffectState> {
static bool FromV8(v8::Isolate* isolate,
v8::Handle<v8::Value> 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<ElectronNSWindow*>(nswindow) shell];
return window && !window->has_frame();
}
bool IsPanel(NSWindow* window) {
return [window isKindOfClass:[NSPanel class]];
}
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<decltype(&SetFrameSize)>(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<decltype(&SetFrameSize)>(
[[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<decltype(&ViewDidMoveToSuperview)>(
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<NSWindow*>* children = [[child_window parentWindow] childWindows];
std::vector<std::pair<NSInteger, NSWindow*>> 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<ElectronNSWindow*>(
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) {
// If we're a panel window, we do not want to activate the app,
// which enables Electron-apps to build Spotlight-like experiences.
//
// On macOS < Sonoma, "activateIgnoringOtherApps:NO" would not
// activate apps if focusing a window that is inActive. That
// changed with macOS Sonoma, which also deprecated
// "activateIgnoringOtherApps". For the panel-specific usecase,
// we can simply replace "activateIgnoringOtherApps:NO" with
// "activate". For details on why we cannot replace all calls 1:1,
// please see
// https://github.com/electron/electron/pull/40307#issuecomment-1801976591.
//
// There's a slim chance we should have never called
// activateIgnoringOtherApps, but we tried that many years ago
// and saw weird focus bugs on other macOS versions. So, to make
// this safe, we're gating by versions.
if (@available(macOS 14.0, *)) {
if (!IsPanel(window_)) {
[[NSApplication sharedApplication] activate];
} else {
[[NSApplication sharedApplication] activateIgnoringOtherApps:NO];
}
} else {
[[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() {
set_wants_to_be_visible(true);
// 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 theres no saved user state because there has been no previous
// > zoom,the size and location of the window dont 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<NativeWindowMac*>(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<CGColorRef> cgcolor(
skia::CGColorCreateFromSkColor(color));
[[[window_ contentView] layer] setBackgroundColor:cgcolor.get()];
}
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<NSProgressIndicator*>(
[[[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<gfx::Point> position) {
traffic_light_position_ = std::move(position);
if (buttons_proxy_) {
[buttons_proxy_ setMargin:traffic_light_position_];
NotifyLayoutWindowControlsOverlay();
}
}
absl::optional<gfx::Point> 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<gin_helper::PersistentDictionary> 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<std::string> 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<NativeWindowMac> const native_window_;
};
std::unique_ptr<views::NonClientFrameView>
NativeWindowMac::CreateNonClientFrameView(views::Widget* widget) {
return std::make_unique<NativeAppWindowFrameViewMac>(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<gfx::Rect> 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