From 09a9d9b54c9800469521105925e2907fe16feaed Mon Sep 17 00:00:00 2001 From: Marshall Greenblatt Date: Thu, 8 Apr 2021 19:15:51 -0400 Subject: [PATCH] chrome: Support customization of context menus (see issue #2969) --- BUILD.gn | 4 + .../chrome_browser_main_extra_parts_cef.cc | 2 + .../chrome/chrome_context_menu_handler.cc | 207 +++++++ .../chrome/chrome_context_menu_handler.h | 16 + libcef/browser/simple_menu_model_impl.cc | 529 ++++++++++++++++++ libcef/browser/simple_menu_model_impl.h | 166 ++++++ patch/patch.cfg | 5 + .../chrome_browser_context_menus.patch | 214 +++++++ tests/cefclient/browser/client_handler.cc | 28 +- tests/cefclient/browser/main_context.h | 3 + tests/cefclient/browser/main_context_impl.cc | 4 + tests/cefclient/browser/main_context_impl.h | 1 + 12 files changed, 1169 insertions(+), 10 deletions(-) create mode 100644 libcef/browser/chrome/chrome_context_menu_handler.cc create mode 100644 libcef/browser/chrome/chrome_context_menu_handler.h create mode 100644 libcef/browser/simple_menu_model_impl.cc create mode 100644 libcef/browser/simple_menu_model_impl.h create mode 100644 patch/patches/chrome_browser_context_menus.patch diff --git a/BUILD.gn b/BUILD.gn index ec53103d2..51fdb0016 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -461,6 +461,8 @@ static_library("libcef_static") { "libcef/browser/chrome/chrome_browser_main_extra_parts_cef.h", "libcef/browser/chrome/chrome_content_browser_client_cef.cc", "libcef/browser/chrome/chrome_content_browser_client_cef.h", + "libcef/browser/chrome/chrome_context_menu_handler.cc", + "libcef/browser/chrome/chrome_context_menu_handler.h", "libcef/browser/chrome_crash_reporter_client_stub.cc", "libcef/browser/context.cc", "libcef/browser/context.h", @@ -637,6 +639,8 @@ static_library("libcef_static") { "libcef/browser/scheme_impl.cc", "libcef/browser/server_impl.cc", "libcef/browser/server_impl.h", + "libcef/browser/simple_menu_model_impl.cc", + "libcef/browser/simple_menu_model_impl.h", "libcef/browser/speech_recognition_manager_delegate.cc", "libcef/browser/speech_recognition_manager_delegate.h", "libcef/browser/ssl_host_state_delegate.cc", diff --git a/libcef/browser/chrome/chrome_browser_main_extra_parts_cef.cc b/libcef/browser/chrome/chrome_browser_main_extra_parts_cef.cc index bd54e5730..411279bce 100644 --- a/libcef/browser/chrome/chrome_browser_main_extra_parts_cef.cc +++ b/libcef/browser/chrome/chrome_browser_main_extra_parts_cef.cc @@ -4,6 +4,7 @@ #include "libcef/browser/chrome/chrome_browser_main_extra_parts_cef.h" +#include "libcef/browser/chrome/chrome_context_menu_handler.h" #include "libcef/browser/context.h" #include "libcef/browser/net/chrome_scheme_handler.h" @@ -34,4 +35,5 @@ void ChromeBrowserMainExtraPartsCef::PreMainMessageLoopRun() { base::TaskShutdownBehavior::BLOCK_SHUTDOWN, base::MayBlock()}); scheme::RegisterWebUIControllerFactory(); + context_menu::RegisterMenuCreatedCallback(); } diff --git a/libcef/browser/chrome/chrome_context_menu_handler.cc b/libcef/browser/chrome/chrome_context_menu_handler.cc new file mode 100644 index 000000000..557ebadc1 --- /dev/null +++ b/libcef/browser/chrome/chrome_context_menu_handler.cc @@ -0,0 +1,207 @@ +// Copyright (c) 2021 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that can +// be found in the LICENSE file. + +#include "libcef/browser/chrome/chrome_context_menu_handler.h" + +#include "libcef/browser/browser_host_base.h" +#include "libcef/browser/context_menu_params_impl.h" +#include "libcef/browser/simple_menu_model_impl.h" + +#include "chrome/browser/renderer_context_menu/render_view_context_menu.h" + +namespace context_menu { + +namespace { + +// Lifespan is controlled by RenderViewContextMenu. +class CefContextMenuObserver : public RenderViewContextMenuObserver, + public CefSimpleMenuModelImpl::StateDelegate { + public: + CefContextMenuObserver(RenderViewContextMenu* context_menu, + CefRefPtr browser, + CefRefPtr handler) + : context_menu_(context_menu), browser_(browser), handler_(handler) {} + + // RenderViewContextMenuObserver methods: + + void InitMenu(const content::ContextMenuParams& params) override { + params_ = new CefContextMenuParamsImpl( + const_cast(&context_menu_->params())); + model_ = new CefSimpleMenuModelImpl( + const_cast(&context_menu_->menu_model()), + context_menu_, this, /*is_owned=*/false, /*is_popup=*/false); + + handler_->OnBeforeContextMenu(browser_, GetFrame(), params_, model_); + } + + bool IsCommandIdSupported(int command_id) override { + // Always claim support for the reserved user ID range. + if (command_id >= MENU_ID_USER_FIRST && command_id <= MENU_ID_USER_LAST) + return true; + + // Also claim support in specific cases where an ItemInfo exists. + return GetItemInfo(command_id) != nullptr; + } + + // Only called if IsCommandIdSupported() returns true. + bool IsCommandIdEnabled(int command_id) override { + // Always return true to use the SimpleMenuModel state. + return true; + } + + // Only called if IsCommandIdSupported() returns true. + bool IsCommandIdChecked(int command_id) override { + auto* info = GetItemInfo(command_id); + return info ? info->checked : false; + } + + // Only called if IsCommandIdSupported() returns true. + bool GetAccelerator(int command_id, ui::Accelerator* accel) override { + auto* info = GetItemInfo(command_id); + if (info && info->accel) { + *accel = *info->accel; + return true; + } + return false; + } + + void CommandWillBeExecuted(int command_id) override { + if (handler_->OnContextMenuCommand(browser_, GetFrame(), params_, + command_id, EVENTFLAG_NONE)) { + // Create an ItemInfo so that we get the ExecuteCommand() callback + // instead of the default handler. + GetOrCreateItemInfo(command_id); + } + } + + // Only called if IsCommandIdSupported() returns true. + void ExecuteCommand(int command_id) override { + auto* info = GetItemInfo(command_id); + if (info) { + // In case it was added in CommandWillBeExecuted(). + MaybeDeleteItemInfo(command_id, info); + } + } + + void OnMenuClosed() override { + handler_->OnContextMenuDismissed(browser_, GetFrame()); + model_->Detach(); + + // Clear stored state because this object won't be deleted until a new + // context menu is created or the associated browser is destroyed. + browser_ = nullptr; + handler_ = nullptr; + params_ = nullptr; + model_ = nullptr; + iteminfomap_.clear(); + } + + // CefSimpleMenuModelImpl::StateDelegate methods: + + void SetChecked(int command_id, bool checked) override { + // No-op if already at the default state. + if (!checked && !GetItemInfo(command_id)) + return; + + auto* info = GetOrCreateItemInfo(command_id); + info->checked = checked; + if (!checked) + MaybeDeleteItemInfo(command_id, info); + } + + void SetAccelerator(int command_id, + base::Optional accel) override { + // No-op if already at the default state. + if (!accel && !GetItemInfo(command_id)) + return; + + auto* info = GetOrCreateItemInfo(command_id); + info->accel = accel; + if (!accel) + MaybeDeleteItemInfo(command_id, info); + } + + private: + struct ItemInfo { + ItemInfo() {} + + bool checked = false; + base::Optional accel; + }; + + ItemInfo* GetItemInfo(int command_id) { + auto it = iteminfomap_.find(command_id); + if (it != iteminfomap_.end()) { + return &it->second; + } + return nullptr; + } + + ItemInfo* GetOrCreateItemInfo(int command_id) { + if (auto info = GetItemInfo(command_id)) + return info; + + auto result = iteminfomap_.insert(std::make_pair(command_id, ItemInfo())); + return &result.first->second; + } + + void MaybeDeleteItemInfo(int command_id, ItemInfo* info) { + // Remove if all info has reverted to the default state. + if (!info->checked && !info->accel) { + auto it = iteminfomap_.find(command_id); + iteminfomap_.erase(it); + } + } + + CefRefPtr GetFrame() const { + CefRefPtr frame; + + // May return nullptr if the frame is destroyed while the menu is pending. + auto* rfh = context_menu_->GetRenderFrameHost(); + if (rfh) { + frame = browser_->GetFrameForHost(rfh); + } + if (!frame) { + frame = browser_->GetMainFrame(); + } + return frame; + } + + RenderViewContextMenu* const context_menu_; + CefRefPtr browser_; + CefRefPtr handler_; + CefRefPtr params_; + CefRefPtr model_; + + // Map of command_id to ItemInfo. + using ItemInfoMap = std::map; + ItemInfoMap iteminfomap_; + + DISALLOW_COPY_AND_ASSIGN(CefContextMenuObserver); +}; + +std::unique_ptr MenuCreatedCallback( + RenderViewContextMenu* context_menu) { + auto browser = CefBrowserHostBase::GetBrowserForContents( + context_menu->source_web_contents()); + if (browser) { + if (auto client = browser->GetClient()) { + if (auto handler = client->GetContextMenuHandler()) { + return std::make_unique(context_menu, browser, + handler); + } + } + } + + return nullptr; +} + +} // namespace + +void RegisterMenuCreatedCallback() { + RenderViewContextMenu::RegisterMenuCreatedCallback( + base::BindRepeating(&MenuCreatedCallback)); +} + +} // namespace context_menu diff --git a/libcef/browser/chrome/chrome_context_menu_handler.h b/libcef/browser/chrome/chrome_context_menu_handler.h new file mode 100644 index 000000000..12c31f196 --- /dev/null +++ b/libcef/browser/chrome/chrome_context_menu_handler.h @@ -0,0 +1,16 @@ +// Copyright (c) 2021 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that can +// be found in the LICENSE file. + +#ifndef CEF_LIBCEF_BROWSER_CHROME_CHROME_CONTEXT_MENU_HANDLER_H_ +#define CEF_LIBCEF_BROWSER_CHROME_CHROME_CONTEXT_MENU_HANDLER_H_ +#pragma once + +namespace context_menu { + +// Register the context menu created callback. +void RegisterMenuCreatedCallback(); + +} // namespace context_menu + +#endif // CEF_LIBCEF_BROWSER_CHROME_CHROME_CONTEXT_MENU_HANDLER_H_ diff --git a/libcef/browser/simple_menu_model_impl.cc b/libcef/browser/simple_menu_model_impl.cc new file mode 100644 index 000000000..4af530cd0 --- /dev/null +++ b/libcef/browser/simple_menu_model_impl.cc @@ -0,0 +1,529 @@ +// Copyright (c) 2021 The Chromium Embedded Framework Authors. +// Portions copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "libcef/browser/simple_menu_model_impl.h" + +#include + +#include "libcef/browser/thread_util.h" +#include "libcef/common/task_runner_impl.h" + +namespace { + +// Value based on the documentation on SimpleMenuModel::GetIndexOfCommandId(). +const int kInvalidIndex = -1; +const int kInvalidCommandId = -1; +const int kInvalidGroupId = -1; + +cef_menu_item_type_t GetCefItemType(ui::MenuModel::ItemType type) { + switch (type) { + case ui::MenuModel::TYPE_COMMAND: + return MENUITEMTYPE_COMMAND; + case ui::MenuModel::TYPE_CHECK: + return MENUITEMTYPE_CHECK; + case ui::MenuModel::TYPE_RADIO: + return MENUITEMTYPE_RADIO; + case ui::MenuModel::TYPE_SEPARATOR: + return MENUITEMTYPE_SEPARATOR; + case ui::MenuModel::TYPE_SUBMENU: + return MENUITEMTYPE_SUBMENU; + default: + return MENUITEMTYPE_NONE; + } +} + +} // namespace + +CefSimpleMenuModelImpl::CefSimpleMenuModelImpl( + ui::SimpleMenuModel* model, + ui::SimpleMenuModel::Delegate* delegate, + StateDelegate* state_delegate, + bool is_owned, + bool is_submenu) + : supported_thread_id_(base::PlatformThread::CurrentId()), + model_(model), + delegate_(delegate), + state_delegate_(state_delegate), + is_owned_(is_owned), + is_submenu_(is_submenu) { + DCHECK(model_); + DCHECK(delegate_); + DCHECK(state_delegate_); +} + +CefSimpleMenuModelImpl::~CefSimpleMenuModelImpl() { + // Detach() must be called before object destruction. + DCHECK(!model_); + DCHECK(submenumap_.empty()); +} + +void CefSimpleMenuModelImpl::Detach() { + DCHECK(VerifyContext()); + + if (!submenumap_.empty()) { + auto it = submenumap_.begin(); + for (; it != submenumap_.end(); ++it) { + it->second->Detach(); + } + submenumap_.clear(); + } + + if (is_owned_) + delete model_; + model_ = nullptr; +} + +bool CefSimpleMenuModelImpl::IsSubMenu() { + if (!VerifyContext()) + return false; + return is_submenu_; +} + +bool CefSimpleMenuModelImpl::Clear() { + if (!VerifyContext()) + return false; + + model_->Clear(); + return true; +} + +int CefSimpleMenuModelImpl::GetCount() { + if (!VerifyContext()) + return 0; + + return model_->GetItemCount(); +} + +bool CefSimpleMenuModelImpl::AddSeparator() { + if (!VerifyContext()) + return false; + + model_->AddSeparator(ui::NORMAL_SEPARATOR); + return true; +} + +bool CefSimpleMenuModelImpl::AddItem(int command_id, const CefString& label) { + if (!VerifyContext()) + return false; + + model_->AddItem(command_id, label); + return true; +} + +bool CefSimpleMenuModelImpl::AddCheckItem(int command_id, + const CefString& label) { + if (!VerifyContext()) + return false; + + model_->AddCheckItem(command_id, label); + return true; +} + +bool CefSimpleMenuModelImpl::AddRadioItem(int command_id, + const CefString& label, + int group_id) { + if (!VerifyContext()) + return false; + + model_->AddRadioItem(command_id, label, group_id); + return true; +} + +CefRefPtr CefSimpleMenuModelImpl::AddSubMenu( + int command_id, + const CefString& label) { + if (!VerifyContext()) + return nullptr; + + auto new_menu = CreateNewSubMenu(nullptr); + model_->AddSubMenu(command_id, label, new_menu->model()); + return new_menu; +} + +bool CefSimpleMenuModelImpl::InsertSeparatorAt(int index) { + if (!VerifyContext()) + return false; + + model_->InsertSeparatorAt(index, ui::NORMAL_SEPARATOR); + return true; +} + +bool CefSimpleMenuModelImpl::InsertItemAt(int index, + int command_id, + const CefString& label) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->InsertItemAt(index, command_id, label); + return true; +} + +bool CefSimpleMenuModelImpl::InsertCheckItemAt(int index, + int command_id, + const CefString& label) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->InsertCheckItemAt(index, command_id, label); + return true; +} + +bool CefSimpleMenuModelImpl::InsertRadioItemAt(int index, + int command_id, + const CefString& label, + int group_id) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->InsertRadioItemAt(index, command_id, label, group_id); + return true; +} + +CefRefPtr CefSimpleMenuModelImpl::InsertSubMenuAt( + int index, + int command_id, + const CefString& label) { + if (!VerifyContext() || !ValidIndex(index)) + return nullptr; + + auto new_menu = CreateNewSubMenu(nullptr); + model_->InsertSubMenuAt(index, command_id, label, new_menu->model()); + return new_menu; +} + +bool CefSimpleMenuModelImpl::Remove(int command_id) { + return RemoveAt(GetIndexOf(command_id)); +} + +bool CefSimpleMenuModelImpl::RemoveAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + auto* sub_menu = + static_cast(model_->GetSubmenuModelAt(index)); + if (sub_menu) { + auto it = submenumap_.find(sub_menu); + if (it != submenumap_.end()) { + it->second->Detach(); + submenumap_.erase(it); + } + } + + model_->RemoveItemAt(index); + return true; +} + +int CefSimpleMenuModelImpl::GetIndexOf(int command_id) { + if (!VerifyContext()) + return kInvalidIndex; + + return model_->GetIndexOfCommandId(command_id); +} + +int CefSimpleMenuModelImpl::GetCommandIdAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return kInvalidCommandId; + + return model_->GetCommandIdAt(index); +} + +bool CefSimpleMenuModelImpl::SetCommandIdAt(int index, int command_id) { + NOTIMPLEMENTED(); + return false; +} + +CefString CefSimpleMenuModelImpl::GetLabel(int command_id) { + return GetLabelAt(GetIndexOf(command_id)); +} + +CefString CefSimpleMenuModelImpl::GetLabelAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return CefString(); + + return model_->GetLabelAt(index); +} + +bool CefSimpleMenuModelImpl::SetLabel(int command_id, const CefString& label) { + return SetLabelAt(GetIndexOf(command_id), label); +} + +bool CefSimpleMenuModelImpl::SetLabelAt(int index, const CefString& label) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->SetLabel(index, label); + return true; +} + +CefSimpleMenuModelImpl::MenuItemType CefSimpleMenuModelImpl::GetType( + int command_id) { + return GetTypeAt(GetIndexOf(command_id)); +} + +CefSimpleMenuModelImpl::MenuItemType CefSimpleMenuModelImpl::GetTypeAt( + int index) { + if (!VerifyContext() || !ValidIndex(index)) + return MENUITEMTYPE_NONE; + + return GetCefItemType(model_->GetTypeAt(index)); +} + +int CefSimpleMenuModelImpl::GetGroupId(int command_id) { + return GetGroupIdAt(GetIndexOf(command_id)); +} + +int CefSimpleMenuModelImpl::GetGroupIdAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return kInvalidGroupId; + + return model_->GetGroupIdAt(index); +} + +bool CefSimpleMenuModelImpl::SetGroupId(int command_id, int group_id) { + return SetGroupIdAt(GetIndexOf(command_id), group_id); +} + +bool CefSimpleMenuModelImpl::SetGroupIdAt(int index, int group_id) { + NOTIMPLEMENTED(); + return false; +} + +CefRefPtr CefSimpleMenuModelImpl::GetSubMenu(int command_id) { + return GetSubMenuAt(GetIndexOf(command_id)); +} + +CefRefPtr CefSimpleMenuModelImpl::GetSubMenuAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return nullptr; + + auto* sub_model = + static_cast(model_->GetSubmenuModelAt(index)); + auto it = submenumap_.find(sub_model); + if (it != submenumap_.end()) + return it->second; + return CreateNewSubMenu(sub_model); +} + +bool CefSimpleMenuModelImpl::IsVisible(int command_id) { + return IsVisibleAt(GetIndexOf(command_id)); +} + +bool CefSimpleMenuModelImpl::IsVisibleAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + return model_->IsVisibleAt(index); +} + +bool CefSimpleMenuModelImpl::SetVisible(int command_id, bool visible) { + return SetVisibleAt(GetIndexOf(command_id), visible); +} + +bool CefSimpleMenuModelImpl::SetVisibleAt(int index, bool visible) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->SetVisibleAt(index, visible); + return true; +} + +bool CefSimpleMenuModelImpl::IsEnabled(int command_id) { + return IsEnabledAt(GetIndexOf(command_id)); +} + +bool CefSimpleMenuModelImpl::IsEnabledAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + return model_->IsEnabledAt(index); +} + +bool CefSimpleMenuModelImpl::SetEnabled(int command_id, bool enabled) { + return SetEnabledAt(GetIndexOf(command_id), enabled); +} + +bool CefSimpleMenuModelImpl::SetEnabledAt(int index, bool enabled) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + model_->SetEnabledAt(index, enabled); + return true; +} + +bool CefSimpleMenuModelImpl::IsChecked(int command_id) { + return IsCheckedAt(GetIndexOf(command_id)); +} + +bool CefSimpleMenuModelImpl::IsCheckedAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + return model_->IsItemCheckedAt(index); +} + +bool CefSimpleMenuModelImpl::SetChecked(int command_id, bool checked) { + if (!VerifyContext() || command_id == kInvalidIndex) + return false; + + state_delegate_->SetChecked(command_id, checked); + return true; +} + +bool CefSimpleMenuModelImpl::SetCheckedAt(int index, bool checked) { + return SetChecked(GetCommandIdAt(index), checked); +} + +bool CefSimpleMenuModelImpl::HasAccelerator(int command_id) { + return HasAcceleratorAt(GetIndexOf(command_id)); +} + +bool CefSimpleMenuModelImpl::HasAcceleratorAt(int index) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + ui::Accelerator accelerator; + return model_->GetAcceleratorAt(index, &accelerator); +} + +bool CefSimpleMenuModelImpl::SetAccelerator(int command_id, + int key_code, + bool shift_pressed, + bool ctrl_pressed, + bool alt_pressed) { + if (!VerifyContext() || command_id == kInvalidIndex) + return false; + + int modifiers = 0; + if (shift_pressed) + modifiers |= ui::EF_SHIFT_DOWN; + if (ctrl_pressed) + modifiers |= ui::EF_CONTROL_DOWN; + if (alt_pressed) + modifiers |= ui::EF_ALT_DOWN; + + state_delegate_->SetAccelerator( + command_id, + ui::Accelerator(static_cast(key_code), modifiers)); + return true; +} + +bool CefSimpleMenuModelImpl::SetAcceleratorAt(int index, + int key_code, + bool shift_pressed, + bool ctrl_pressed, + bool alt_pressed) { + return SetAccelerator(GetCommandIdAt(index), key_code, shift_pressed, + ctrl_pressed, alt_pressed); +} + +bool CefSimpleMenuModelImpl::RemoveAccelerator(int command_id) { + if (!VerifyContext() || command_id == kInvalidIndex) + return false; + state_delegate_->SetAccelerator(command_id, base::nullopt); + return true; +} + +bool CefSimpleMenuModelImpl::RemoveAcceleratorAt(int index) { + return RemoveAccelerator(GetCommandIdAt(index)); +} + +bool CefSimpleMenuModelImpl::GetAccelerator(int command_id, + int& key_code, + bool& shift_pressed, + bool& ctrl_pressed, + bool& alt_pressed) { + return GetAcceleratorAt(GetIndexOf(command_id), key_code, shift_pressed, + ctrl_pressed, alt_pressed); +} + +bool CefSimpleMenuModelImpl::GetAcceleratorAt(int index, + int& key_code, + bool& shift_pressed, + bool& ctrl_pressed, + bool& alt_pressed) { + if (!VerifyContext() || !ValidIndex(index)) + return false; + + ui::Accelerator accel; + if (model_->GetAcceleratorAt(index, &accel)) { + key_code = accel.key_code(); + shift_pressed = accel.modifiers() & ui::EF_SHIFT_DOWN; + ctrl_pressed = accel.modifiers() & ui::EF_CONTROL_DOWN; + alt_pressed = accel.modifiers() & ui::EF_ALT_DOWN; + return true; + } + return false; +} + +bool CefSimpleMenuModelImpl::SetColor(int command_id, + cef_menu_color_type_t color_type, + cef_color_t color) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::SetColorAt(int index, + cef_menu_color_type_t color_type, + cef_color_t color) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::GetColor(int command_id, + cef_menu_color_type_t color_type, + cef_color_t& color) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::GetColorAt(int index, + cef_menu_color_type_t color_type, + cef_color_t& color) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::SetFontList(int command_id, + const CefString& font_list) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::SetFontListAt(int index, + const CefString& font_list) { + NOTIMPLEMENTED(); + return false; +} + +bool CefSimpleMenuModelImpl::VerifyContext() { + if (base::PlatformThread::CurrentId() != supported_thread_id_) { + // This object should only be accessed from the thread that created it. + NOTREACHED(); + return false; + } + + if (!model_) + return false; + + return true; +} + +bool CefSimpleMenuModelImpl::ValidIndex(int index) { + return index > kInvalidIndex && index < model_->GetItemCount(); +} + +CefRefPtr CefSimpleMenuModelImpl::CreateNewSubMenu( + ui::SimpleMenuModel* model) { + bool is_owned = false; + if (!model) { + model = new ui::SimpleMenuModel(delegate_); + is_owned = true; + } + + CefRefPtr new_impl = new CefSimpleMenuModelImpl( + model, delegate_, state_delegate_, is_owned, /*is_submodel=*/true); + submenumap_.insert(std::make_pair(model, new_impl)); + return new_impl; +} diff --git a/libcef/browser/simple_menu_model_impl.h b/libcef/browser/simple_menu_model_impl.h new file mode 100644 index 000000000..66cfed5e6 --- /dev/null +++ b/libcef/browser/simple_menu_model_impl.h @@ -0,0 +1,166 @@ +// Copyright (c) 2021 The Chromium Embedded Framework Authors. +// Portions copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEF_LIBCEF_BROWSER_SIMPLE_MENU_MODEL_IMPL_H_ +#define CEF_LIBCEF_BROWSER_SIMPLE_MENU_MODEL_IMPL_H_ +#pragma once + +#include + +#include "include/cef_menu_model.h" + +#include "base/threading/platform_thread.h" +#include "ui/base/models/simple_menu_model.h" + +// Implementation of CefMenuModel that wraps an existing ui::SimpleMenuModel. +class CefSimpleMenuModelImpl : public CefMenuModel { + public: + // Interface for setting state using CefMenuModel methods that will later be + // retrieved via the ui::SimpleMenuModel::Delegate implementation. + class StateDelegate { + public: + virtual void SetChecked(int command_id, bool checked) = 0; + virtual void SetAccelerator(int command_id, + base::Optional accel) = 0; + + protected: + virtual ~StateDelegate() {} + }; + + // |delegate| should be the same that was used to create |model|. + // If |is_owned| is true then |model| will be deleted on Detach(). + CefSimpleMenuModelImpl(ui::SimpleMenuModel* model, + ui::SimpleMenuModel::Delegate* delegate, + StateDelegate* state_delegate, + bool is_owned, + bool is_submenu); + ~CefSimpleMenuModelImpl() override; + + // Must be called before the object is deleted. + void Detach(); + + // CefMenuModel methods. + bool IsSubMenu() override; + bool Clear() override; + int GetCount() override; + bool AddSeparator() override; + bool AddItem(int command_id, const CefString& label) override; + bool AddCheckItem(int command_id, const CefString& label) override; + bool AddRadioItem(int command_id, + const CefString& label, + int group_id) override; + CefRefPtr AddSubMenu(int command_id, + const CefString& label) override; + bool InsertSeparatorAt(int index) override; + bool InsertItemAt(int index, int command_id, const CefString& label) override; + bool InsertCheckItemAt(int index, + int command_id, + const CefString& label) override; + bool InsertRadioItemAt(int index, + int command_id, + const CefString& label, + int group_id) override; + CefRefPtr InsertSubMenuAt(int index, + int command_id, + const CefString& label) override; + bool Remove(int command_id) override; + bool RemoveAt(int index) override; + int GetIndexOf(int command_id) override; + int GetCommandIdAt(int index) override; + bool SetCommandIdAt(int index, int command_id) override; + CefString GetLabel(int command_id) override; + CefString GetLabelAt(int index) override; + bool SetLabel(int command_id, const CefString& label) override; + bool SetLabelAt(int index, const CefString& label) override; + MenuItemType GetType(int command_id) override; + MenuItemType GetTypeAt(int index) override; + int GetGroupId(int command_id) override; + int GetGroupIdAt(int index) override; + bool SetGroupId(int command_id, int group_id) override; + bool SetGroupIdAt(int index, int group_id) override; + CefRefPtr GetSubMenu(int command_id) override; + CefRefPtr GetSubMenuAt(int index) override; + bool IsVisible(int command_id) override; + bool IsVisibleAt(int index) override; + bool SetVisible(int command_id, bool visible) override; + bool SetVisibleAt(int index, bool visible) override; + bool IsEnabled(int command_id) override; + bool IsEnabledAt(int index) override; + bool SetEnabled(int command_id, bool enabled) override; + bool SetEnabledAt(int index, bool enabled) override; + bool IsChecked(int command_id) override; + bool IsCheckedAt(int index) override; + bool SetChecked(int command_id, bool checked) override; + bool SetCheckedAt(int index, bool checked) override; + bool HasAccelerator(int command_id) override; + bool HasAcceleratorAt(int index) override; + bool SetAccelerator(int command_id, + int key_code, + bool shift_pressed, + bool ctrl_pressed, + bool alt_pressed) override; + bool SetAcceleratorAt(int index, + int key_code, + bool shift_pressed, + bool ctrl_pressed, + bool alt_pressed) override; + bool RemoveAccelerator(int command_id) override; + bool RemoveAcceleratorAt(int index) override; + bool GetAccelerator(int command_id, + int& key_code, + bool& shift_pressed, + bool& ctrl_pressed, + bool& alt_pressed) override; + bool GetAcceleratorAt(int index, + int& key_code, + bool& shift_pressed, + bool& ctrl_pressed, + bool& alt_pressed) override; + bool SetColor(int command_id, + cef_menu_color_type_t color_type, + cef_color_t color) override; + bool SetColorAt(int index, + cef_menu_color_type_t color_type, + cef_color_t color) override; + bool GetColor(int command_id, + cef_menu_color_type_t color_type, + cef_color_t& color) override; + bool GetColorAt(int index, + cef_menu_color_type_t color_type, + cef_color_t& color) override; + bool SetFontList(int command_id, const CefString& font_list) override; + bool SetFontListAt(int index, const CefString& font_list) override; + + ui::SimpleMenuModel* model() const { return model_; } + + private: + // Verify that the object is attached and being accessed from the correct + // thread. + bool VerifyContext(); + + // Returns true if |index| is valid. + bool ValidIndex(int index); + + CefRefPtr CreateNewSubMenu( + ui::SimpleMenuModel* model); + + base::PlatformThreadId supported_thread_id_; + + ui::SimpleMenuModel* model_; + ui::SimpleMenuModel::Delegate* const delegate_; + StateDelegate* const state_delegate_; + const bool is_owned_; + const bool is_submenu_; + + // Keep the submenus alive until they're removed, or we're destroyed. + using SubMenuMap = + std::map>; + SubMenuMap submenumap_; + + IMPLEMENT_REFCOUNTING(CefSimpleMenuModelImpl); + DISALLOW_COPY_AND_ASSIGN(CefSimpleMenuModelImpl); +}; + +#endif // CEF_LIBCEF_BROWSER_SIMPLE_MENU_MODEL_IMPL_H_ diff --git a/patch/patch.cfg b/patch/patch.cfg index cc04727d1..d1f9a3e14 100644 --- a/patch/patch.cfg +++ b/patch/patch.cfg @@ -222,6 +222,11 @@ patches = [ # NavigationTest.LoadCrossOriginLoadURL with the chrome runtime. 'name': 'chrome_browser_content_settings', }, + { + # chrome: Support custom handling of context menus. + # https://bitbucket.org/chromiumembedded/cef/issues/2969 + 'name': 'chrome_browser_context_menus', + }, { # Don't initialize ExtensionSystemFactory when extensions are disabled. # https://bitbucket.org/chromiumembedded/cef/issues/2852 diff --git a/patch/patches/chrome_browser_context_menus.patch b/patch/patches/chrome_browser_context_menus.patch new file mode 100644 index 000000000..e6ecec585 --- /dev/null +++ b/patch/patches/chrome_browser_context_menus.patch @@ -0,0 +1,214 @@ +diff --git chrome/browser/renderer_context_menu/render_view_context_menu.cc chrome/browser/renderer_context_menu/render_view_context_menu.cc +index 86f785a8658f..e7693b4a5efc 100644 +--- chrome/browser/renderer_context_menu/render_view_context_menu.cc ++++ chrome/browser/renderer_context_menu/render_view_context_menu.cc +@@ -253,6 +253,13 @@ base::OnceCallback* GetMenuShownCallback() { + return callback.get(); + } + ++ ++RenderViewContextMenu::MenuCreatedCallback* GetMenuCreatedCallback() { ++ static base::NoDestructor ++ callback; ++ return callback.get(); ++} ++ + enum class UmaEnumIdLookupType { + GeneralEnumId, + ContextSpecificEnumId, +@@ -461,6 +468,10 @@ int FindUMAEnumValueForCommand(int id, UmaEnumIdLookupType type) { + if (ContextMenuMatcher::IsExtensionsCustomCommandId(id)) + return 1; + ++ // Match the MENU_ID_USER_FIRST to MENU_ID_USER_LAST range from cef_types.h. ++ if (id >= 26500 && id <= 28500) ++ return 1; ++ + id = CollapseCommandsForUMA(id); + const auto& map = GetIdcToUmaMap(type); + auto it = map.find(id); +@@ -616,6 +627,14 @@ RenderViewContextMenu::RenderViewContextMenu( + } + set_content_type( + ContextMenuContentTypeFactory::Create(source_web_contents_, params)); ++ ++ auto* cb = GetMenuCreatedCallback(); ++ if (!cb->is_null()) { ++ first_observer_ = cb->Run(this); ++ if (first_observer_) { ++ observers_.AddObserver(first_observer_.get()); ++ } ++ } + } + + RenderViewContextMenu::~RenderViewContextMenu() = default; +@@ -966,6 +985,12 @@ void RenderViewContextMenu::InitMenu() { + // menu, meaning that each menu item added/removed in this function will cause + // it to visibly jump on the screen (see b/173569669). + AppendQuickAnswersItems(); ++ ++ if (first_observer_) { ++ // Do this last so that the observer can optionally modify previously ++ // created items. ++ first_observer_->InitMenu(params_); ++ } + } + + Profile* RenderViewContextMenu::GetProfile() const { +@@ -2590,6 +2615,12 @@ void RenderViewContextMenu::RegisterMenuShownCallbackForTesting( + *GetMenuShownCallback() = std::move(cb); + } + ++// static ++void RenderViewContextMenu::RegisterMenuCreatedCallback( ++ MenuCreatedCallback cb) { ++ *GetMenuCreatedCallback() = cb; ++} ++ + ProtocolHandlerRegistry::ProtocolHandlerList + RenderViewContextMenu::GetHandlersForLinkUrl() { + ProtocolHandlerRegistry::ProtocolHandlerList handlers = +diff --git chrome/browser/renderer_context_menu/render_view_context_menu.h chrome/browser/renderer_context_menu/render_view_context_menu.h +index 25f331b10b01..5ea2e8ba1c7a 100644 +--- chrome/browser/renderer_context_menu/render_view_context_menu.h ++++ chrome/browser/renderer_context_menu/render_view_context_menu.h +@@ -92,6 +92,12 @@ class RenderViewContextMenu : public RenderViewContextMenuBase { + static void RegisterMenuShownCallbackForTesting( + base::OnceCallback cb); + ++ // Registers a callback that will be called each time a context menu is ++ // created. ++ using MenuCreatedCallback = base::RepeatingCallback< ++ std::unique_ptr(RenderViewContextMenu*)>; ++ static void RegisterMenuCreatedCallback(MenuCreatedCallback cb); ++ + protected: + Profile* GetProfile() const; + +@@ -266,6 +272,9 @@ class RenderViewContextMenu : public RenderViewContextMenuBase { + ui::SimpleMenuModel protocol_handler_submenu_model_; + ProtocolHandlerRegistry* protocol_handler_registry_; + ++ // An observer returned via MenuCreatedCallback that will be called first. ++ std::unique_ptr first_observer_; ++ + // An observer that handles spelling suggestions, "Add to dictionary", and + // "Use enhanced spell check" items. + std::unique_ptr spelling_suggestions_menu_observer_; +diff --git chrome/browser/ui/views/renderer_context_menu/render_view_context_menu_views.cc chrome/browser/ui/views/renderer_context_menu/render_view_context_menu_views.cc +index f6ff626c408e..b9300419672d 100644 +--- chrome/browser/ui/views/renderer_context_menu/render_view_context_menu_views.cc ++++ chrome/browser/ui/views/renderer_context_menu/render_view_context_menu_views.cc +@@ -136,6 +136,9 @@ void RenderViewContextMenuViews::RunMenuAt(views::Widget* parent, + bool RenderViewContextMenuViews::GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accel) const { ++ if (RenderViewContextMenu::GetAcceleratorForCommandId(command_id, accel)) ++ return true; ++ + // There are no formally defined accelerators we can query so we assume + // that Ctrl+C, Ctrl+V, Ctrl+X, Ctrl-A, etc do what they normally do. + switch (command_id) { +diff --git components/renderer_context_menu/render_view_context_menu_base.cc components/renderer_context_menu/render_view_context_menu_base.cc +index d8ef3850f0ad..d18382efec7f 100644 +--- components/renderer_context_menu/render_view_context_menu_base.cc ++++ components/renderer_context_menu/render_view_context_menu_base.cc +@@ -352,6 +352,17 @@ bool RenderViewContextMenuBase::IsCommandIdChecked(int id) const { + return false; + } + ++bool RenderViewContextMenuBase::GetAcceleratorForCommandId( ++ int id, ++ ui::Accelerator* accelerator) const { ++ for (auto& observer : observers_) { ++ if (observer.IsCommandIdSupported(id)) ++ return observer.GetAccelerator(id, accelerator); ++ } ++ ++ return false; ++} ++ + void RenderViewContextMenuBase::ExecuteCommand(int id, int event_flags) { + command_executed_ = true; + RecordUsedItem(id); +diff --git components/renderer_context_menu/render_view_context_menu_base.h components/renderer_context_menu/render_view_context_menu_base.h +index 2dca58a6553e..1d139e77da21 100644 +--- components/renderer_context_menu/render_view_context_menu_base.h ++++ components/renderer_context_menu/render_view_context_menu_base.h +@@ -82,6 +82,9 @@ class RenderViewContextMenuBase : public ui::SimpleMenuModel::Delegate, + + const ui::SimpleMenuModel& menu_model() const { return menu_model_; } + const content::ContextMenuParams& params() const { return params_; } ++ content::WebContents* source_web_contents() const { ++ return source_web_contents_; ++ } + + // Returns true if the specified command id is known and valid for + // this menu. If the command is known |enabled| is set to indicate +@@ -90,6 +93,9 @@ class RenderViewContextMenuBase : public ui::SimpleMenuModel::Delegate, + + // SimpleMenuModel::Delegate implementation. + bool IsCommandIdChecked(int command_id) const override; ++ bool GetAcceleratorForCommandId( ++ int command_id, ++ ui::Accelerator* accelerator) const override; + void ExecuteCommand(int command_id, int event_flags) override; + void OnMenuWillShow(ui::SimpleMenuModel* source) override; + void MenuClosed(ui::SimpleMenuModel* source) override; +@@ -119,6 +125,9 @@ class RenderViewContextMenuBase : public ui::SimpleMenuModel::Delegate, + content::WebContents* GetWebContents() const override; + content::BrowserContext* GetBrowserContext() const override; + ++ // May return nullptr if the frame was deleted while the menu was open. ++ content::RenderFrameHost* GetRenderFrameHost() const; ++ + protected: + friend class RenderViewContextMenuTest; + friend class RenderViewContextMenuPrefsTest; +@@ -156,9 +165,6 @@ class RenderViewContextMenuBase : public ui::SimpleMenuModel::Delegate, + // TODO(oshima): Remove this. + virtual void AppendPlatformEditableItems() {} + +- // May return nullptr if the frame was deleted while the menu was open. +- content::RenderFrameHost* GetRenderFrameHost() const; +- + bool IsCustomItemChecked(int id) const; + bool IsCustomItemEnabled(int id) const; + +diff --git components/renderer_context_menu/render_view_context_menu_observer.cc components/renderer_context_menu/render_view_context_menu_observer.cc +index 2e2d05f91c64..85b256b2be9b 100644 +--- components/renderer_context_menu/render_view_context_menu_observer.cc ++++ components/renderer_context_menu/render_view_context_menu_observer.cc +@@ -15,3 +15,8 @@ bool RenderViewContextMenuObserver::IsCommandIdChecked(int command_id) { + bool RenderViewContextMenuObserver::IsCommandIdEnabled(int command_id) { + return false; + } ++ ++bool RenderViewContextMenuObserver::GetAccelerator(int command_id, ++ ui::Accelerator* accel) { ++ return false; ++} +diff --git components/renderer_context_menu/render_view_context_menu_observer.h components/renderer_context_menu/render_view_context_menu_observer.h +index b360a8eb4e82..6f9023a62904 100644 +--- components/renderer_context_menu/render_view_context_menu_observer.h ++++ components/renderer_context_menu/render_view_context_menu_observer.h +@@ -11,6 +11,10 @@ namespace content { + struct ContextMenuParams; + } + ++namespace ui { ++class Accelerator; ++} ++ + // The interface used for implementing context-menu items. The following + // instruction describe how to implement a context-menu item with this + // interface. +@@ -100,6 +104,8 @@ class RenderViewContextMenuObserver { + virtual bool IsCommandIdChecked(int command_id); + virtual bool IsCommandIdEnabled(int command_id); + ++ virtual bool GetAccelerator(int command_id, ui::Accelerator* accel); ++ + // Called when a user selects the specified context-menu item. This is + // only called when the observer returns true for IsCommandIdSupported() + // for that |command_id|. diff --git a/tests/cefclient/browser/client_handler.cc b/tests/cefclient/browser/client_handler.cc index 8828bac51..f7a666bbd 100644 --- a/tests/cefclient/browser/client_handler.cc +++ b/tests/cefclient/browser/client_handler.cc @@ -321,24 +321,32 @@ void ClientHandler::OnBeforeContextMenu(CefRefPtr browser, if (model->GetCount() > 0) model->AddSeparator(); - // Add DevTools items to all context menus. - model->AddItem(CLIENT_ID_SHOW_DEVTOOLS, "&Show DevTools"); - model->AddItem(CLIENT_ID_CLOSE_DEVTOOLS, "Close DevTools"); - model->AddSeparator(); - model->AddItem(CLIENT_ID_INSPECT_ELEMENT, "Inspect Element"); + const bool use_chrome_runtime = MainContext::Get()->UseChromeRuntime(); + if (!use_chrome_runtime) { + // TODO(chrome-runtime): Add support for this. + // Add DevTools items to all context menus. + model->AddItem(CLIENT_ID_SHOW_DEVTOOLS, "&Show DevTools"); + model->AddItem(CLIENT_ID_CLOSE_DEVTOOLS, "Close DevTools"); + model->AddSeparator(); + model->AddItem(CLIENT_ID_INSPECT_ELEMENT, "Inspect Element"); + } if (HasSSLInformation(browser)) { model->AddSeparator(); model->AddItem(CLIENT_ID_SHOW_SSL_INFO, "Show SSL information"); } - model->AddSeparator(); - model->AddItem(CLIENT_ID_CURSOR_CHANGE_DISABLED, "Cursor change disabled"); - if (mouse_cursor_change_disabled_) - model->SetChecked(CLIENT_ID_CURSOR_CHANGE_DISABLED, true); + if (!use_chrome_runtime) { + // TODO(chrome-runtime): Add support for this. + model->AddSeparator(); + model->AddCheckItem(CLIENT_ID_CURSOR_CHANGE_DISABLED, + "Cursor change disabled"); + if (mouse_cursor_change_disabled_) + model->SetChecked(CLIENT_ID_CURSOR_CHANGE_DISABLED, true); + } model->AddSeparator(); - model->AddItem(CLIENT_ID_OFFLINE, "Offline mode"); + model->AddCheckItem(CLIENT_ID_OFFLINE, "Offline mode"); if (offline_) model->SetChecked(CLIENT_ID_OFFLINE, true); diff --git a/tests/cefclient/browser/main_context.h b/tests/cefclient/browser/main_context.h index 780bf1efc..1837f4afa 100644 --- a/tests/cefclient/browser/main_context.h +++ b/tests/cefclient/browser/main_context.h @@ -38,6 +38,9 @@ class MainContext { // Returns the background color. virtual cef_color_t GetBackgroundColor() = 0; + // Returns true if the Chrome runtime will be used. + virtual bool UseChromeRuntime() = 0; + // Returns true if the Views framework will be used. virtual bool UseViews() = 0; diff --git a/tests/cefclient/browser/main_context_impl.cc b/tests/cefclient/browser/main_context_impl.cc index e309e0ee9..77995d592 100644 --- a/tests/cefclient/browser/main_context_impl.cc +++ b/tests/cefclient/browser/main_context_impl.cc @@ -171,6 +171,10 @@ cef_color_t MainContextImpl::GetBackgroundColor() { return background_color_; } +bool MainContextImpl::UseChromeRuntime() { + return use_chrome_runtime_; +} + bool MainContextImpl::UseViews() { return use_views_; } diff --git a/tests/cefclient/browser/main_context_impl.h b/tests/cefclient/browser/main_context_impl.h index 008629b5b..f0035a9cc 100644 --- a/tests/cefclient/browser/main_context_impl.h +++ b/tests/cefclient/browser/main_context_impl.h @@ -27,6 +27,7 @@ class MainContextImpl : public MainContext { std::string GetAppWorkingDirectory() OVERRIDE; std::string GetMainURL() OVERRIDE; cef_color_t GetBackgroundColor() OVERRIDE; + bool UseChromeRuntime() OVERRIDE; bool UseViews() OVERRIDE; bool UseWindowlessRendering() OVERRIDE; bool TouchEventsEnabled() OVERRIDE;