mirror of
				https://bitbucket.org/chromiumembedded/cef
				synced 2025-06-05 21:39:12 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			475 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			475 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // 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/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/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"
 | |
| #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<cef_event_flags_t>(0);
 | |
| 
 | |
| class CefRunContextMenuCallbackImpl : public CefRunContextMenuCallback {
 | |
|  public:
 | |
|   typedef base::OnceCallback<void(int, cef_event_flags_t)> Callback;
 | |
| 
 | |
|   explicit CefRunContextMenuCallbackImpl(Callback callback)
 | |
|       : callback_(std::move(callback)) {}
 | |
| 
 | |
|   ~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);
 | |
|   DISALLOW_COPY_AND_ASSIGN(CefRunContextMenuCallbackImpl);
 | |
| };
 | |
| 
 | |
| }  // namespace
 | |
| 
 | |
| CefMenuManager::CefMenuManager(AlloyBrowserHostImpl* 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::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.
 | |
|       ignore_result(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.
 | |
|       ignore_result(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_.link_followed);
 | |
| }
 | |
| 
 | |
| bool CefMenuManager::FormatLabel(CefRefPtr<CefMenuModelImpl> 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<int>(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<int>(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<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;
 | |
| }
 |