feat: add support for associating a Menu with a WebFrameMain (#45138)

* feat: add support for associating a Menu with a WebFrameMain

This allows certain OS level features to activate such as Writing Tools, Autofill.. and Services.

There appears to be a bug in macOS where the responder chain isn't traversed if the menu is not popped up using an event, as such we spoof a fake mouse event at the write coordinates in the right window and use that to open the menu.

* build: fix build on non-mac

* build: oops missed a header

* fix: safely handle optional T* by checking nullptr too

* build: fix gn check and build errors

* docs: suggested changes

* feat: default `frame` to `window.webContents.mainFrame` when possible

* fix: avoid deref nullptr view

* Revert "feat: default `frame` to `window.webContents.mainFrame` when possible"

This reverts commit 2e888368199317d67f6ad931a7e9eff0295c4b1b.

* fix: lint

* Remove redundant scoped objects

This code, including the comments, matches almost exactly the behavior of this argument to the function.

* Add ScopedPumpMessagesInPrivateModes patch

* More null pointer safety

---------

Co-authored-by: clavin <clavin@electronjs.org>
This commit is contained in:
Samuel Attard 2025-03-28 11:50:07 -07:00 committed by GitHub
parent 46b108e9a4
commit 49aba471dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 104 additions and 16 deletions

View file

@ -7,6 +7,7 @@
#include <utility>
#include "shell/browser/api/electron_api_base_window.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/api/ui_event.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/native_window.h"
@ -16,6 +17,7 @@
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_converters/optional_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"

View file

@ -22,6 +22,7 @@ class Arguments;
namespace electron::api {
class BaseWindow;
class WebFrameMain;
class Menu : public gin::Wrappable<Menu>,
public gin_helper::EventEmitterMixin<Menu>,
@ -81,6 +82,7 @@ class Menu : public gin::Wrappable<Menu>,
void OnMenuWillShow(ui::SimpleMenuModel* source) override;
virtual void PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
int y,
int positioning_item,

View file

@ -13,6 +13,7 @@
namespace electron {
class NativeWindow;
class WebFrameMain;
namespace api {
@ -23,12 +24,14 @@ class MenuMac : public Menu {
// Menu
void PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
int y,
int positioning_item,
ui::mojom::MenuSourceType source_type,
base::OnceClosure callback) override;
void PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
const base::WeakPtr<WebFrameMain>& frame,
int32_t window_id,
int x,
int y,

View file

@ -11,11 +11,15 @@
#include "base/strings/sys_string_conversions.h"
#include "base/task/current_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h" // nogncheck
#include "content/browser/renderer_host/render_widget_host_view_mac.h" // nogncheck
#include "content/public/browser/browser_task_traits.h"
#include "shell/browser/api/electron_api_base_window.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/native_window.h"
#include "shell/common/keyboard_util.h"
#include "shell/common/node_includes.h"
#include "ui/base/cocoa/menu_utils.h"
namespace {
@ -48,6 +52,7 @@ MenuMac::MenuMac(gin::Arguments* args) : Menu(args) {}
MenuMac::~MenuMac() = default;
void MenuMac::PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
int y,
int positioning_item,
@ -57,14 +62,19 @@ void MenuMac::PopupAt(BaseWindow* window,
if (!native_window)
return;
base::WeakPtr<WebFrameMain> weak_frame;
if (frame && frame.value()) {
weak_frame = frame.value()->GetWeakPtr();
}
// Make sure the Menu object would not be garbage-collected until the callback
// has run.
base::OnceClosure callback_with_ref = BindSelfToClosure(std::move(callback));
auto popup =
base::BindOnce(&MenuMac::PopupOnUI, weak_factory_.GetWeakPtr(),
native_window->GetWeakPtr(), window->weak_map_id(), x, y,
positioning_item, std::move(callback_with_ref));
auto popup = base::BindOnce(&MenuMac::PopupOnUI, weak_factory_.GetWeakPtr(),
native_window->GetWeakPtr(), weak_frame,
window->weak_map_id(), x, y, positioning_item,
std::move(callback_with_ref));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(popup));
}
@ -95,6 +105,7 @@ v8::Local<v8::Value> Menu::GetUserAcceleratorAt(int command_id) const {
}
void MenuMac::PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
const base::WeakPtr<WebFrameMain>& frame,
int32_t window_id,
int x,
int y,
@ -145,18 +156,27 @@ void MenuMac::PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
position.x = position.x - [menu size].width;
[popup_controllers_[window_id] setCloseCallback:std::move(close_callback)];
// Make sure events can be pumped while the menu is up.
base::CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
// One of the events that could be pumped is |window.close()|.
// User-initiated event-tracking loops protect against this by
// setting flags in -[CrApplication sendEvent:], but since
// web-content menus are initiated by IPC message the setup has to
// be done manually.
base::mac::ScopedSendingEvent sendingEventScoper;
if (frame && frame->render_frame_host()) {
auto* rfh = frame->render_frame_host()->GetOutermostMainFrameOrEmbedder();
if (rfh && rfh->IsRenderFrameLive()) {
auto* rwhvm =
static_cast<content::RenderWidgetHostViewMac*>(rfh->GetView());
RenderWidgetHostViewCocoa* cocoa_view = rwhvm->GetInProcessNSView();
view = cocoa_view;
}
}
// Don't emit unresponsive event when showing menu.
[menu popUpMenuPositioningItem:item atLocation:position inView:view];
NSEvent* dummy_event = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
location:position
modifierFlags:0
timestamp:0
windowNumber:nswindow.windowNumber
context:nil
eventNumber:0
clickCount:1
pressure:0];
ui::ShowContextMenu(menu, dummy_event, view, true);
}
void MenuMac::ClosePopupAt(int32_t window_id) {

View file

@ -8,6 +8,7 @@
#include <utility>
#include "shell/browser/api/electron_api_base_window.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/native_window_views.h"
#include "ui/display/screen.h"
@ -20,6 +21,7 @@ MenuViews::MenuViews(gin::Arguments* args) : Menu(args) {}
MenuViews::~MenuViews() = default;
void MenuViews::PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
int y,
int positioning_item,

View file

@ -22,6 +22,7 @@ class MenuViews : public Menu {
protected:
// Menu
void PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
int y,
int positioning_item,

View file

@ -73,6 +73,10 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
content::RenderFrameHost* render_frame_host() const;
base::WeakPtr<WebFrameMain> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
// disable copy
WebFrameMain(const WebFrameMain&) = delete;
WebFrameMain& operator=(const WebFrameMain&) = delete;