// Copyright (c) 2012 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/menu_manager.h" #include #include #include "libcef/browser/alloy/alloy_browser_host_impl.h" #include "libcef/browser/context_menu_params_impl.h" #include "libcef/browser/menu_runner.h" #include "libcef/browser/thread_util.h" #include "libcef/common/app_manager.h" #include "base/logging.h" #include "cef/grit/cef_strings.h" #include "chrome/grit/generated_resources.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_widget_host_view.h" #include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h" namespace { CefString GetLabel(int message_id) { std::u16string label = CefAppManager::Get()->GetContentClient()->GetLocalizedString(message_id); DCHECK(!label.empty()); return label; } const int kInvalidCommandId = -1; const cef_event_flags_t kEmptyEventFlags = static_cast(0); class CefRunContextMenuCallbackImpl : public CefRunContextMenuCallback { public: using Callback = base::OnceCallback; explicit CefRunContextMenuCallbackImpl(Callback callback) : callback_(std::move(callback)) {} CefRunContextMenuCallbackImpl(const CefRunContextMenuCallbackImpl&) = delete; CefRunContextMenuCallbackImpl& operator=( const CefRunContextMenuCallbackImpl&) = delete; ~CefRunContextMenuCallbackImpl() { if (!callback_.is_null()) { // The callback is still pending. Cancel it now. if (CEF_CURRENTLY_ON_UIT()) { RunNow(std::move(callback_), kInvalidCommandId, kEmptyEventFlags); } else { CEF_POST_TASK(CEF_UIT, base::BindOnce(&CefRunContextMenuCallbackImpl::RunNow, std::move(callback_), kInvalidCommandId, kEmptyEventFlags)); } } } void Continue(int command_id, cef_event_flags_t event_flags) override { if (CEF_CURRENTLY_ON_UIT()) { if (!callback_.is_null()) { RunNow(std::move(callback_), command_id, event_flags); callback_.Reset(); } } else { CEF_POST_TASK(CEF_UIT, base::BindOnce(&CefRunContextMenuCallbackImpl::Continue, this, command_id, event_flags)); } } void Cancel() override { Continue(kInvalidCommandId, kEmptyEventFlags); } void Disconnect() { callback_.Reset(); } private: static void RunNow(Callback callback, int command_id, cef_event_flags_t event_flags) { CEF_REQUIRE_UIT(); std::move(callback).Run(command_id, event_flags); } Callback callback_; IMPLEMENT_REFCOUNTING(CefRunContextMenuCallbackImpl); }; } // namespace CefMenuManager::CefMenuManager(AlloyBrowserHostImpl* browser, std::unique_ptr runner) : content::WebContentsObserver(browser->web_contents()), browser_(browser), runner_(std::move(runner)), custom_menu_callback_(nullptr), weak_ptr_factory_(this) { DCHECK(web_contents()); model_ = new CefMenuModelImpl(this, nullptr, false); } CefMenuManager::~CefMenuManager() { // The model may outlive the delegate if the context menu is visible when the // application is closed. model_->set_delegate(nullptr); } void CefMenuManager::Destroy() { CancelContextMenu(); if (runner_) { runner_.reset(nullptr); } } bool CefMenuManager::IsShowingContextMenu() { if (!web_contents()) { return false; } return web_contents()->IsShowingContextMenu(); } bool CefMenuManager::CreateContextMenu( const content::ContextMenuParams& params) { // The renderer may send the "show context menu" message multiple times, one // for each right click mouse event it receives. Normally, this doesn't happen // because mouse events are not forwarded once the context menu is showing. // However, there's a race - the context menu may not yet be showing when // the second mouse event arrives. In this case, |HandleContextMenu()| will // get called multiple times - if so, don't create another context menu. // TODO(asvitkine): Fix the renderer so that it doesn't do this. if (IsShowingContextMenu()) { return true; } params_ = params; model_->Clear(); // Create the default menu model. CreateDefaultModel(); bool custom_menu = false; DCHECK(!custom_menu_callback_); // Give the client a chance to modify the model. CefRefPtr client = browser_->GetClient(); if (client.get()) { CefRefPtr handler = client->GetContextMenuHandler(); if (handler.get()) { CefRefPtr paramsPtr( new CefContextMenuParamsImpl(¶ms_)); CefRefPtr frame = browser_->GetFocusedFrame(); handler->OnBeforeContextMenu(browser_, frame, paramsPtr.get(), model_.get()); MenuWillShow(model_); if (model_->GetCount() > 0) { CefRefPtr callbackImpl( new CefRunContextMenuCallbackImpl( base::BindOnce(&CefMenuManager::ExecuteCommandCallback, weak_ptr_factory_.GetWeakPtr()))); // This reference will be cleared when the callback is executed or // the callback object is deleted. custom_menu_callback_ = callbackImpl.get(); if (handler->RunContextMenu(browser_, frame, paramsPtr.get(), model_.get(), callbackImpl.get())) { custom_menu = true; } else { // Callback should not be executed if the handler returns false. DCHECK(custom_menu_callback_); custom_menu_callback_ = nullptr; callbackImpl->Disconnect(); } } // Do not keep references to the parameters in the callback. std::ignore = paramsPtr->Detach(nullptr); DCHECK(paramsPtr->HasOneRef()); DCHECK(model_->VerifyRefCount()); // Menu is empty so notify the client and return. if (model_->GetCount() == 0 && !custom_menu) { MenuClosed(model_); return true; } } } if (custom_menu) { return true; } if (!runner_ || !runner_->RunContextMenu(browser_, model_.get(), params_)) { LOG(ERROR) << "Default context menu implementation is not available; " "canceling the menu"; return false; } return true; } void CefMenuManager::CancelContextMenu() { if (IsShowingContextMenu()) { if (custom_menu_callback_) { custom_menu_callback_->Cancel(); } else if (runner_) { runner_->CancelContextMenu(); } } } void CefMenuManager::ExecuteCommand(CefRefPtr source, int command_id, cef_event_flags_t event_flags) { // Give the client a chance to handle the command. CefRefPtr client = browser_->GetClient(); if (client.get()) { CefRefPtr handler = client->GetContextMenuHandler(); if (handler.get()) { CefRefPtr paramsPtr( new CefContextMenuParamsImpl(¶ms_)); bool handled = handler->OnContextMenuCommand( browser_, browser_->GetFocusedFrame(), paramsPtr.get(), command_id, event_flags); // Do not keep references to the parameters in the callback. std::ignore = paramsPtr->Detach(nullptr); DCHECK(paramsPtr->HasOneRef()); if (handled) { return; } } } // Execute the default command handling. ExecuteDefaultCommand(command_id); } void CefMenuManager::MenuWillShow(CefRefPtr source) { // May be called for sub-menus as well. if (source.get() != model_.get()) { return; } if (!web_contents()) { return; } // May be called multiple times. if (IsShowingContextMenu()) { return; } // Notify the host before showing the context menu. web_contents()->SetShowingContextMenu(true); } void CefMenuManager::MenuClosed(CefRefPtr source) { // May be called for sub-menus as well. if (source.get() != model_.get()) { return; } if (!web_contents()) { return; } DCHECK(IsShowingContextMenu()); // Notify the client. CefRefPtr client = browser_->GetClient(); if (client.get()) { CefRefPtr handler = client->GetContextMenuHandler(); if (handler.get()) { handler->OnContextMenuDismissed(browser_, browser_->GetFocusedFrame()); } } // Notify the host after closing the context menu. web_contents()->SetShowingContextMenu(false); web_contents()->NotifyContextMenuClosed(params_.link_followed); } bool CefMenuManager::FormatLabel(CefRefPtr source, std::u16string& label) { if (!runner_) { return false; } return runner_->FormatLabel(label); } void CefMenuManager::ExecuteCommandCallback(int command_id, cef_event_flags_t event_flags) { DCHECK(IsShowingContextMenu()); DCHECK(custom_menu_callback_); if (command_id != kInvalidCommandId) { ExecuteCommand(model_, command_id, event_flags); } MenuClosed(model_); custom_menu_callback_ = nullptr; } void CefMenuManager::CreateDefaultModel() { if (!params_.custom_items.empty()) { // Custom menu items originating from the renderer process. For example, // plugin placeholder menu items. for (auto& item : params_.custom_items) { auto new_item = item->Clone(); new_item->action += MENU_ID_CUSTOM_FIRST; DCHECK_LE(static_cast(new_item->action), MENU_ID_CUSTOM_LAST); model_->AddMenuItem(*new_item); } return; } if (params_.is_editable) { // Editable node. model_->AddItem(MENU_ID_UNDO, GetLabel(IDS_CONTENT_CONTEXT_UNDO)); model_->AddItem(MENU_ID_REDO, GetLabel(IDS_CONTENT_CONTEXT_REDO)); model_->AddSeparator(); model_->AddItem(MENU_ID_CUT, GetLabel(IDS_CONTENT_CONTEXT_CUT)); model_->AddItem(MENU_ID_COPY, GetLabel(IDS_CONTENT_CONTEXT_COPY)); model_->AddItem(MENU_ID_PASTE, GetLabel(IDS_CONTENT_CONTEXT_PASTE)); model_->AddSeparator(); model_->AddItem(MENU_ID_SELECT_ALL, GetLabel(IDS_CONTENT_CONTEXT_SELECTALL)); if (!(params_.edit_flags & CM_EDITFLAG_CAN_UNDO)) { model_->SetEnabled(MENU_ID_UNDO, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_REDO)) { model_->SetEnabled(MENU_ID_REDO, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_CUT)) { model_->SetEnabled(MENU_ID_CUT, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_COPY)) { model_->SetEnabled(MENU_ID_COPY, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_PASTE)) { model_->SetEnabled(MENU_ID_PASTE, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_DELETE)) { model_->SetEnabled(MENU_ID_DELETE, false); } if (!(params_.edit_flags & CM_EDITFLAG_CAN_SELECT_ALL)) { model_->SetEnabled(MENU_ID_SELECT_ALL, false); } if (!params_.misspelled_word.empty()) { // Always add a separator before the list of dictionary suggestions or // "No spelling suggestions". model_->AddSeparator(); if (!params_.dictionary_suggestions.empty()) { for (size_t i = 0; i < params_.dictionary_suggestions.size() && MENU_ID_SPELLCHECK_SUGGESTION_0 + i <= MENU_ID_SPELLCHECK_SUGGESTION_LAST; ++i) { model_->AddItem(MENU_ID_SPELLCHECK_SUGGESTION_0 + static_cast(i), params_.dictionary_suggestions[i]); } // When there are dictionary suggestions add a separator before "Add to // dictionary". model_->AddSeparator(); } else { model_->AddItem(MENU_ID_NO_SPELLING_SUGGESTIONS, GetLabel(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS)); model_->SetEnabled(MENU_ID_NO_SPELLING_SUGGESTIONS, false); } model_->AddItem(MENU_ID_ADD_TO_DICTIONARY, GetLabel(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY)); } } else if (!params_.selection_text.empty()) { // Something is selected. model_->AddItem(MENU_ID_COPY, GetLabel(IDS_CONTENT_CONTEXT_COPY)); } else if (!params_.page_url.is_empty() || !params_.frame_url.is_empty()) { // Page or frame. model_->AddItem(MENU_ID_BACK, GetLabel(IDS_CONTENT_CONTEXT_BACK)); model_->AddItem(MENU_ID_FORWARD, GetLabel(IDS_CONTENT_CONTEXT_FORWARD)); model_->AddSeparator(); model_->AddItem(MENU_ID_PRINT, GetLabel(IDS_CONTENT_CONTEXT_PRINT)); model_->AddItem(MENU_ID_VIEW_SOURCE, GetLabel(IDS_CONTENT_CONTEXT_VIEWPAGESOURCE)); if (!browser_->CanGoBack()) { model_->SetEnabled(MENU_ID_BACK, false); } if (!browser_->CanGoForward()) { model_->SetEnabled(MENU_ID_FORWARD, false); } } } void CefMenuManager::ExecuteDefaultCommand(int command_id) { if (IsCustomContextMenuCommand(command_id)) { if (web_contents()) { web_contents()->ExecuteCustomContextMenuCommand( command_id - MENU_ID_CUSTOM_FIRST, params_.link_followed); } return; } // If the user chose a replacement word for a misspelling, replace it here. if (command_id >= MENU_ID_SPELLCHECK_SUGGESTION_0 && command_id <= MENU_ID_SPELLCHECK_SUGGESTION_LAST) { const size_t suggestion_index = static_cast(command_id) - MENU_ID_SPELLCHECK_SUGGESTION_0; if (suggestion_index < params_.dictionary_suggestions.size()) { browser_->ReplaceMisspelling( params_.dictionary_suggestions[suggestion_index]); } return; } switch (command_id) { // Navigation. case MENU_ID_BACK: browser_->GoBack(); break; case MENU_ID_FORWARD: browser_->GoForward(); break; case MENU_ID_RELOAD: browser_->Reload(); break; case MENU_ID_RELOAD_NOCACHE: browser_->ReloadIgnoreCache(); break; case MENU_ID_STOPLOAD: browser_->StopLoad(); break; // Editing. case MENU_ID_UNDO: browser_->GetFocusedFrame()->Undo(); break; case MENU_ID_REDO: browser_->GetFocusedFrame()->Redo(); break; case MENU_ID_CUT: browser_->GetFocusedFrame()->Cut(); break; case MENU_ID_COPY: browser_->GetFocusedFrame()->Copy(); break; case MENU_ID_PASTE: browser_->GetFocusedFrame()->Paste(); break; case MENU_ID_DELETE: browser_->GetFocusedFrame()->Delete(); break; case MENU_ID_SELECT_ALL: browser_->GetFocusedFrame()->SelectAll(); break; // Miscellaneous. case MENU_ID_FIND: // TODO(cef): Implement. NOTIMPLEMENTED(); break; case MENU_ID_PRINT: browser_->Print(); break; case MENU_ID_VIEW_SOURCE: browser_->GetFocusedFrame()->ViewSource(); break; // Spell checking. case MENU_ID_ADD_TO_DICTIONARY: browser_->GetHost()->AddWordToDictionary(params_.misspelled_word); break; default: break; } } bool CefMenuManager::IsCustomContextMenuCommand(int command_id) { // Verify that the command ID is in the correct range. if (command_id < MENU_ID_CUSTOM_FIRST || command_id > MENU_ID_CUSTOM_LAST) { return false; } command_id -= MENU_ID_CUSTOM_FIRST; // Verify that the specific command ID was passed from the renderer process. if (!params_.custom_items.empty()) { for (size_t i = 0; i < params_.custom_items.size(); ++i) { if (static_cast(params_.custom_items[i]->action) == command_id) { return true; } } } return false; }