| // 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 <utility> |
| |
| #include "libcef/browser/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/content_client.h" |
| |
| #include "base/compiler_specific.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" |
| |
| namespace { |
| |
| CefString GetLabel(int message_id) { |
| base::string16 label = |
| CefContentClient::Get()->GetLocalizedString(message_id); |
| DCHECK(!label.empty()); |
| return label; |
| } |
| |
| const int kInvalidCommandId = -1; |
| const cef_event_flags_t kEmptyEventFlags = static_cast<cef_event_flags_t>(0); |
| |
| class CefRunContextMenuCallbackImpl : public CefRunContextMenuCallback { |
| public: |
| typedef base::Callback<void(int, cef_event_flags_t)> Callback; |
| |
| explicit CefRunContextMenuCallbackImpl(const Callback& callback) |
| : callback_(callback) {} |
| |
| ~CefRunContextMenuCallbackImpl() { |
| if (!callback_.is_null()) { |
| // The callback is still pending. Cancel it now. |
| if (CEF_CURRENTLY_ON_UIT()) { |
| RunNow(callback_, kInvalidCommandId, kEmptyEventFlags); |
| } else { |
| CEF_POST_TASK( |
| CEF_UIT, |
| base::Bind(&CefRunContextMenuCallbackImpl::RunNow, 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(callback_, command_id, event_flags); |
| callback_.Reset(); |
| } |
| } else { |
| CEF_POST_TASK(CEF_UIT, |
| base::Bind(&CefRunContextMenuCallbackImpl::Continue, this, |
| command_id, event_flags)); |
| } |
| } |
| |
| void Cancel() override { Continue(kInvalidCommandId, kEmptyEventFlags); } |
| |
| void Disconnect() { callback_.Reset(); } |
| |
| private: |
| static void RunNow(const Callback& callback, |
| int command_id, |
| cef_event_flags_t event_flags) { |
| CEF_REQUIRE_UIT(); |
| callback.Run(command_id, event_flags); |
| } |
| |
| Callback callback_; |
| |
| IMPLEMENT_REFCOUNTING(CefRunContextMenuCallbackImpl); |
| DISALLOW_COPY_AND_ASSIGN(CefRunContextMenuCallbackImpl); |
| }; |
| |
| } // namespace |
| |
| CefMenuManager::CefMenuManager(CefBrowserHostImpl* browser, |
| std::unique_ptr<CefMenuRunner> 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<CefClient> client = browser_->GetClient(); |
| if (client.get()) { |
| CefRefPtr<CefContextMenuHandler> handler = client->GetContextMenuHandler(); |
| if (handler.get()) { |
| CefRefPtr<CefContextMenuParamsImpl> paramsPtr( |
| new CefContextMenuParamsImpl(¶ms_)); |
| CefRefPtr<CefFrame> frame = browser_->GetFocusedFrame(); |
| |
| handler->OnBeforeContextMenu(browser_, frame, paramsPtr.get(), |
| model_.get()); |
| |
| MenuWillShow(model_); |
| |
| if (model_->GetCount() > 0) { |
| CefRefPtr<CefRunContextMenuCallbackImpl> callbackImpl( |
| new CefRunContextMenuCallbackImpl( |
| base::Bind(&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. |
| 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 || !runner_) |
| return true; |
| return runner_->RunContextMenu(browser_, model_.get(), params_); |
| } |
| |
| void CefMenuManager::CancelContextMenu() { |
| if (IsShowingContextMenu()) { |
| if (custom_menu_callback_) |
| custom_menu_callback_->Cancel(); |
| else if (runner_) |
| runner_->CancelContextMenu(); |
| } |
| } |
| |
| void CefMenuManager::ExecuteCommand(CefRefPtr<CefMenuModelImpl> source, |
| int command_id, |
| cef_event_flags_t event_flags) { |
| // Give the client a chance to handle the command. |
| CefRefPtr<CefClient> client = browser_->GetClient(); |
| if (client.get()) { |
| CefRefPtr<CefContextMenuHandler> handler = client->GetContextMenuHandler(); |
| if (handler.get()) { |
| CefRefPtr<CefContextMenuParamsImpl> 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. |
| paramsPtr->Detach(nullptr); |
| DCHECK(paramsPtr->HasOneRef()); |
| |
| if (handled) |
| return; |
| } |
| } |
| |
| // Execute the default command handling. |
| ExecuteDefaultCommand(command_id); |
| } |
| |
| void CefMenuManager::MenuWillShow(CefRefPtr<CefMenuModelImpl> 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<CefMenuModelImpl> 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<CefClient> client = browser_->GetClient(); |
| if (client.get()) { |
| CefRefPtr<CefContextMenuHandler> 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_.custom_context); |
| } |
| |
| bool CefMenuManager::FormatLabel(CefRefPtr<CefMenuModelImpl> source, |
| base::string16& 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 or Flash menu items. |
| for (size_t i = 0; i < params_.custom_items.size(); ++i) { |
| content::MenuItem menu_item = params_.custom_items[i]; |
| menu_item.action += MENU_ID_CUSTOM_FIRST; |
| DCHECK_LE(static_cast<int>(menu_item.action), MENU_ID_CUSTOM_LAST); |
| model_->AddMenuItem(menu_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<int>(i), |
| params_.dictionary_suggestions[i].c_str()); |
| } |
| |
| // 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_.custom_context); |
| } |
| 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<size_t>(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<int>(params_.custom_items[i].action) == command_id) |
| return true; |
| } |
| } |
| return false; |
| } |