mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
* feat: add app shims on macOS An app shim is a small app, in a bundle of its own, that shows windows on behalf of a host Electron app. The host still creates each window, loads its page and handles its events, but the NSWindow belongs to the shim's process, so the Dock, the app switcher, Mission Control and Stage Manager attribute the window to the shim and not to the host. - `app.startAppShimServer()` / `app.stopAppShimServer()` and the `app-shim-connected` event, which hands over an `AppShimHost`. - A `BaseWindow` / `BrowserWindow` created with the `appShim` option has its native window made in that shim's process, through Chromium's remote_cocoa. Such a window is a new NativeWindow implementation that only goes through views::Widget; what remote_cocoa has no call for is a no-op (see docs/api/app-shim-host.md). - The shim's executable is `Helpers/app_shim_loader` from the framework bundle. It links nothing from Electron: it loads the host's framework and calls the new `ElectronAppShimMain` export, which runs an `app-shim` process that connects back to the host over a Mach bootstrap server. - Electron Framework gains an rpath to the folder it is in, so that it finds its sibling frameworks when an executable in another bundle loads it. Not available in Mac App Store builds. Co-Authored-By: Claude <noreply@anthropic.com> * feat: app shim menus, dialogs, window chrome and review fixes Follow-up to the first app shim commit. Fixes from review: - A window torn down from one of its own listeners (shim.quit() or win.destroy() in `show`) no longer frees the native host that is notifying. - The native close button no longer leaves the shim waiting on an answer that was dropped with the window. - Keys the page does not handle go back to the shim, whose menu they are shortcuts of, and not to the host's menu. - stopAppShimServer() drops shims that have connected and not said hello. - Quitting a shim asks the host without blocking and without cancelling a logout; quit-requested can now be answered with "no". - Size limits and useContentSize are in content sizes; restore() and unmaximize() each do one thing; webContents.isFocused() works. New: - shim.setMenu(): a menu bar of the shim's own, built in the shim from a description of the Menu, with clicks sent back. - Message boxes and open and save dialogs are sheets of the window in the shim's process. - titleBarStyle, traffic light position and visibility, vibrancy, tabs and Quick Look, applied in the shim to the NSWindow that remote_cocoa made. - A Chromium patch for what remote_cocoa has no call for: movable, closable, minimizable and fullscreenable, clearing an aspect ratio, and yielding activation to the hosting process by pid. - A shim that loaded another build of Electron than the running host tells the host and quits: app-shim-version-mismatch. The API is marked experimental in the docs. Co-Authored-By: Claude <noreply@anthropic.com> * fix: address review of app shims - A window in an app shim can't be given a parent, at construction or with setParentWindow(), as the docs say. - shim.setMenu() is now shim.setApplicationMenu(). - A message box that is showing under an id makes way for a new one with that id, whichever process shows it. - Mac App Store builds no longer export the shim's entry point; they already had no loader. - setProgressBar() and setHasShadow() are listed among what does nothing on a window in a shim. - The spec no longer asserts inside an event listener. Co-Authored-By: Claude <noreply@anthropic.com> * fix: clang-tidy findings in app shim code * docs: reword the app shim docs for clarity * chore: update patches Re-exported after rebasing onto Chromium 158.0.8105.0; only line offsets changed. Co-Authored-By: Claude <noreply@anthropic.com> * test: use vitest matchers in the app shim spec --------- Co-authored-by: Claude <noreply@anthropic.com>
232 lines
8.4 KiB
Plaintext
232 lines
8.4 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/ui/message_box.h"
|
|
|
|
#include <utility>
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
#include "base/containers/flat_map.h"
|
|
#include "base/logging.h"
|
|
#include "base/mac/mac_util.h"
|
|
#include "base/no_destructor.h"
|
|
#include "base/strings/sys_string_conversions.h"
|
|
#include "base/task/sequenced_task_runner.h"
|
|
#include "shell/browser/native_window.h"
|
|
#include "shell/browser/native_window_mac_remote.h"
|
|
#include "skia/ext/skia_utils_mac.h"
|
|
#include "ui/gfx/image/image_skia.h"
|
|
|
|
namespace electron {
|
|
|
|
MessageBoxSettings::MessageBoxSettings() = default;
|
|
MessageBoxSettings::MessageBoxSettings(const MessageBoxSettings&) = default;
|
|
MessageBoxSettings::~MessageBoxSettings() = default;
|
|
|
|
namespace {
|
|
|
|
// <ID, messageBox> map
|
|
base::flat_map<int, NSAlert*>& GetDialogsMap() {
|
|
static base::NoDestructor<base::flat_map<int, NSAlert*>> dialogs;
|
|
return *dialogs;
|
|
}
|
|
|
|
NSAlert* CreateNSAlert(const MessageBoxSettings& settings) {
|
|
// Ignore the title; it's the window title on other platforms and ignorable.
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
[alert setMessageText:base::SysUTF8ToNSString(settings.message)];
|
|
[alert setInformativeText:base::SysUTF8ToNSString(settings.detail)];
|
|
|
|
switch (settings.type) {
|
|
case MessageBoxType::kInformation:
|
|
alert.alertStyle = NSAlertStyleInformational;
|
|
break;
|
|
case MessageBoxType::kWarning:
|
|
case MessageBoxType::kError:
|
|
// NSWarningAlertStyle shows the app icon while NSAlertStyleCritical
|
|
// shows a warning icon with an app icon badge. Since there is no
|
|
// error variant, lets just use NSAlertStyleCritical.
|
|
alert.alertStyle = NSAlertStyleCritical;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
for (size_t i = 0; i < settings.buttons.size(); ++i) {
|
|
NSString* title = base::SysUTF8ToNSString(settings.buttons[i]);
|
|
// An empty title causes crash on macOS.
|
|
if (settings.buttons[i].empty())
|
|
title = @"(empty)";
|
|
NSButton* button = [alert addButtonWithTitle:title];
|
|
[button setTag:i];
|
|
}
|
|
|
|
NSArray<NSButton*>* ns_buttons = [alert buttons];
|
|
int button_count = static_cast<int>([ns_buttons count]);
|
|
|
|
if (settings.default_id >= 0 && settings.default_id < button_count) {
|
|
// The first button added gets set as the default selected, so remove
|
|
// that and set the button @ default_id to be default.
|
|
[[ns_buttons objectAtIndex:0] setKeyEquivalent:@""];
|
|
[[ns_buttons objectAtIndex:settings.default_id] setKeyEquivalent:@"\r"];
|
|
}
|
|
|
|
if (button_count > 1 && settings.cancel_id >= 0) {
|
|
// Bind cancel id button to escape key if there is more than one button.
|
|
if (settings.cancel_id < button_count) {
|
|
[[ns_buttons objectAtIndex:settings.cancel_id] setKeyEquivalent:@"\e"];
|
|
}
|
|
|
|
// TODO(@codebytere): This behavior violates HIG & should be deprecated.
|
|
if (settings.cancel_id == settings.default_id) {
|
|
[[ns_buttons objectAtIndex:settings.default_id] highlight:YES];
|
|
}
|
|
}
|
|
|
|
if (!settings.checkbox_label.empty()) {
|
|
alert.showsSuppressionButton = YES;
|
|
alert.suppressionButton.title =
|
|
base::SysUTF8ToNSString(settings.checkbox_label);
|
|
alert.suppressionButton.state = settings.checkbox_checked
|
|
? NSControlStateValueOn
|
|
: NSControlStateValueOff;
|
|
}
|
|
|
|
if (!settings.icon.isNull()) {
|
|
NSImage* image = skia::SkBitmapToNSImage(*settings.icon.bitmap());
|
|
[alert setIcon:image];
|
|
}
|
|
|
|
if (settings.text_width > 0) {
|
|
NSRect rect = NSMakeRect(0, 0, settings.text_width, 0);
|
|
NSView* accessoryView = [[NSView alloc] initWithFrame:rect];
|
|
[alert setAccessoryView:accessoryView];
|
|
}
|
|
|
|
return alert;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int ShowMessageBoxSync(const MessageBoxSettings& settings) {
|
|
if (settings.parent_window && settings.parent_window->IsInAppShim()) {
|
|
return static_cast<NativeWindowMacRemote*>(settings.parent_window)
|
|
->ShowMessageBoxSync(settings);
|
|
}
|
|
|
|
NSAlert* alert(CreateNSAlert(settings));
|
|
|
|
// Use runModal for synchronous alert without parent, since we don't have a
|
|
// window to wait for. Also use it when window is provided but it is not
|
|
// shown as it would be impossible to dismiss the alert if it is connected
|
|
// to invisible window (see #22671).
|
|
if (!settings.parent_window || !settings.parent_window->IsVisible())
|
|
return [alert runModal];
|
|
|
|
__block int ret_code = -1;
|
|
|
|
NSWindow* window =
|
|
settings.parent_window->GetNativeWindow().GetNativeNSWindow();
|
|
[alert beginSheetModalForWindow:window
|
|
completionHandler:^(NSModalResponse response) {
|
|
ret_code = response;
|
|
[NSApp stopModal];
|
|
}];
|
|
|
|
[NSApp runModalForWindow:window];
|
|
return ret_code;
|
|
}
|
|
|
|
void ShowMessageBox(const MessageBoxSettings& settings,
|
|
MessageBoxCallback callback) {
|
|
if (settings.parent_window && settings.parent_window->IsInAppShim()) {
|
|
static_cast<NativeWindowMacRemote*>(settings.parent_window)
|
|
->ShowMessageBox(settings, std::move(callback));
|
|
return;
|
|
}
|
|
ShowMessageBoxForNSWindow(settings,
|
|
settings.parent_window
|
|
? settings.parent_window->GetNativeWindow()
|
|
: gfx::NativeWindow(),
|
|
std::move(callback));
|
|
}
|
|
|
|
void ShowMessageBoxForNSWindow(const MessageBoxSettings& settings,
|
|
gfx::NativeWindow native_window,
|
|
MessageBoxCallback callback) {
|
|
NSAlert* alert = CreateNSAlert(settings);
|
|
NSWindow* window = native_window.GetNativeNSWindow();
|
|
|
|
// Use runModal for synchronous alert without parent, since we don't have a
|
|
// window to wait for.
|
|
if (!window) {
|
|
int ret = [alert runModal];
|
|
std::move(callback).Run(
|
|
ret, alert.suppressionButton.state == NSControlStateValueOn);
|
|
} else {
|
|
if (settings.id) {
|
|
CloseMessageBoxIfShowing(*settings.id);
|
|
GetDialogsMap()[*settings.id] = alert;
|
|
}
|
|
|
|
// Duplicate the callback object here since c is a reference and gcd would
|
|
// only store the pointer, by duplication we can force gcd to store a copy.
|
|
__block MessageBoxCallback callback_ = std::move(callback);
|
|
__block std::optional<int> id = std::move(settings.id);
|
|
__block int cancel_id = settings.cancel_id;
|
|
scoped_refptr<base::SequencedTaskRunner> task_runner =
|
|
base::SequencedTaskRunner::GetCurrentDefault();
|
|
|
|
auto handler = ^(NSModalResponse response) {
|
|
if (id)
|
|
GetDialogsMap().erase(*id);
|
|
// When the alert is cancelled programmatically, the response would be
|
|
// something like -1000. This currently only happens when users call
|
|
// CloseMessageBox API, and we should return cancelId as result.
|
|
if (response < 0)
|
|
response = cancel_id;
|
|
bool suppressed = alert.suppressionButton.state == NSControlStateValueOn;
|
|
// The completionHandler runs inside a transaction commit, and we should
|
|
// not do any runModal inside it. However since we can not control what
|
|
// users will run in the callback, we have to delay running the callback
|
|
// until next tick, otherwise crash like this may happen:
|
|
// https://github.com/electron/electron/issues/26884
|
|
task_runner->PostTask(FROM_HERE, base::BindOnce(std::move(callback_),
|
|
response, suppressed));
|
|
};
|
|
[alert beginSheetModalForWindow:window completionHandler:handler];
|
|
}
|
|
}
|
|
|
|
void CloseMessageBoxIfShowing(int id) {
|
|
if (auto it = GetDialogsMap().find(id); it != GetDialogsMap().end()) {
|
|
[NSApp endSheet:it->second.window];
|
|
return;
|
|
}
|
|
// One that a window in an app shim is showing, in the shim's process.
|
|
NativeWindowMacRemote::CloseMessageBox(id);
|
|
}
|
|
|
|
void CloseMessageBox(int id) {
|
|
auto it = GetDialogsMap().find(id);
|
|
if (it == GetDialogsMap().end() && NativeWindowMacRemote::CloseMessageBox(id))
|
|
return;
|
|
if (it == GetDialogsMap().end()) {
|
|
LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
|
|
return;
|
|
}
|
|
[NSApp endSheet:it->second.window];
|
|
}
|
|
|
|
void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
[alert setMessageText:base::SysUTF16ToNSString(title)];
|
|
[alert setInformativeText:base::SysUTF16ToNSString(content)];
|
|
[alert setAlertStyle:NSAlertStyleCritical];
|
|
[alert runModal];
|
|
}
|
|
|
|
} // namespace electron
|