• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2012 The Chromium Embedded Framework Authors. All rights
2 // reserved. Use of this source code is governed by a BSD-style license that can
3 // be found in the LICENSE file.
4 
5 #include "libcef/browser/menu_manager.h"
6 
7 #include <utility>
8 
9 #include "libcef/browser/alloy/alloy_browser_host_impl.h"
10 #include "libcef/browser/context_menu_params_impl.h"
11 #include "libcef/browser/menu_runner.h"
12 #include "libcef/browser/thread_util.h"
13 #include "libcef/common/app_manager.h"
14 
15 #include "base/compiler_specific.h"
16 #include "base/logging.h"
17 #include "cef/grit/cef_strings.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "content/public/browser/render_frame_host.h"
20 #include "content/public/browser/render_process_host.h"
21 #include "content/public/browser/render_widget_host_view.h"
22 #include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h"
23 
24 namespace {
25 
GetLabel(int message_id)26 CefString GetLabel(int message_id) {
27   std::u16string label =
28       CefAppManager::Get()->GetContentClient()->GetLocalizedString(message_id);
29   DCHECK(!label.empty());
30   return label;
31 }
32 
33 const int kInvalidCommandId = -1;
34 const cef_event_flags_t kEmptyEventFlags = static_cast<cef_event_flags_t>(0);
35 
36 class CefRunContextMenuCallbackImpl : public CefRunContextMenuCallback {
37  public:
38   typedef base::Callback<void(int, cef_event_flags_t)> Callback;
39 
CefRunContextMenuCallbackImpl(const Callback & callback)40   explicit CefRunContextMenuCallbackImpl(const Callback& callback)
41       : callback_(callback) {}
42 
~CefRunContextMenuCallbackImpl()43   ~CefRunContextMenuCallbackImpl() {
44     if (!callback_.is_null()) {
45       // The callback is still pending. Cancel it now.
46       if (CEF_CURRENTLY_ON_UIT()) {
47         RunNow(callback_, kInvalidCommandId, kEmptyEventFlags);
48       } else {
49         CEF_POST_TASK(
50             CEF_UIT,
51             base::Bind(&CefRunContextMenuCallbackImpl::RunNow, callback_,
52                        kInvalidCommandId, kEmptyEventFlags));
53       }
54     }
55   }
56 
Continue(int command_id,cef_event_flags_t event_flags)57   void Continue(int command_id, cef_event_flags_t event_flags) override {
58     if (CEF_CURRENTLY_ON_UIT()) {
59       if (!callback_.is_null()) {
60         RunNow(callback_, command_id, event_flags);
61         callback_.Reset();
62       }
63     } else {
64       CEF_POST_TASK(CEF_UIT,
65                     base::Bind(&CefRunContextMenuCallbackImpl::Continue, this,
66                                command_id, event_flags));
67     }
68   }
69 
Cancel()70   void Cancel() override { Continue(kInvalidCommandId, kEmptyEventFlags); }
71 
Disconnect()72   void Disconnect() { callback_.Reset(); }
73 
74  private:
RunNow(const Callback & callback,int command_id,cef_event_flags_t event_flags)75   static void RunNow(const Callback& callback,
76                      int command_id,
77                      cef_event_flags_t event_flags) {
78     CEF_REQUIRE_UIT();
79     callback.Run(command_id, event_flags);
80   }
81 
82   Callback callback_;
83 
84   IMPLEMENT_REFCOUNTING(CefRunContextMenuCallbackImpl);
85   DISALLOW_COPY_AND_ASSIGN(CefRunContextMenuCallbackImpl);
86 };
87 
88 }  // namespace
89 
CefMenuManager(AlloyBrowserHostImpl * browser,std::unique_ptr<CefMenuRunner> runner)90 CefMenuManager::CefMenuManager(AlloyBrowserHostImpl* browser,
91                                std::unique_ptr<CefMenuRunner> runner)
92     : content::WebContentsObserver(browser->web_contents()),
93       browser_(browser),
94       runner_(std::move(runner)),
95       custom_menu_callback_(nullptr),
96       weak_ptr_factory_(this) {
97   DCHECK(web_contents());
98   model_ = new CefMenuModelImpl(this, nullptr, false);
99 }
100 
~CefMenuManager()101 CefMenuManager::~CefMenuManager() {
102   // The model may outlive the delegate if the context menu is visible when the
103   // application is closed.
104   model_->set_delegate(nullptr);
105 }
106 
Destroy()107 void CefMenuManager::Destroy() {
108   CancelContextMenu();
109   if (runner_)
110     runner_.reset(nullptr);
111 }
112 
IsShowingContextMenu()113 bool CefMenuManager::IsShowingContextMenu() {
114   if (!web_contents())
115     return false;
116   return web_contents()->IsShowingContextMenu();
117 }
118 
CreateContextMenu(const content::ContextMenuParams & params)119 bool CefMenuManager::CreateContextMenu(
120     const content::ContextMenuParams& params) {
121   // The renderer may send the "show context menu" message multiple times, one
122   // for each right click mouse event it receives. Normally, this doesn't happen
123   // because mouse events are not forwarded once the context menu is showing.
124   // However, there's a race - the context menu may not yet be showing when
125   // the second mouse event arrives. In this case, |HandleContextMenu()| will
126   // get called multiple times - if so, don't create another context menu.
127   // TODO(asvitkine): Fix the renderer so that it doesn't do this.
128   if (IsShowingContextMenu())
129     return true;
130 
131   params_ = params;
132   model_->Clear();
133 
134   // Create the default menu model.
135   CreateDefaultModel();
136 
137   bool custom_menu = false;
138   DCHECK(!custom_menu_callback_);
139 
140   // Give the client a chance to modify the model.
141   CefRefPtr<CefClient> client = browser_->GetClient();
142   if (client.get()) {
143     CefRefPtr<CefContextMenuHandler> handler = client->GetContextMenuHandler();
144     if (handler.get()) {
145       CefRefPtr<CefContextMenuParamsImpl> paramsPtr(
146           new CefContextMenuParamsImpl(&params_));
147       CefRefPtr<CefFrame> frame = browser_->GetFocusedFrame();
148 
149       handler->OnBeforeContextMenu(browser_, frame, paramsPtr.get(),
150                                    model_.get());
151 
152       MenuWillShow(model_);
153 
154       if (model_->GetCount() > 0) {
155         CefRefPtr<CefRunContextMenuCallbackImpl> callbackImpl(
156             new CefRunContextMenuCallbackImpl(
157                 base::Bind(&CefMenuManager::ExecuteCommandCallback,
158                            weak_ptr_factory_.GetWeakPtr())));
159 
160         // This reference will be cleared when the callback is executed or
161         // the callback object is deleted.
162         custom_menu_callback_ = callbackImpl.get();
163 
164         if (handler->RunContextMenu(browser_, frame, paramsPtr.get(),
165                                     model_.get(), callbackImpl.get())) {
166           custom_menu = true;
167         } else {
168           // Callback should not be executed if the handler returns false.
169           DCHECK(custom_menu_callback_);
170           custom_menu_callback_ = nullptr;
171           callbackImpl->Disconnect();
172         }
173       }
174 
175       // Do not keep references to the parameters in the callback.
176       paramsPtr->Detach(nullptr);
177       DCHECK(paramsPtr->HasOneRef());
178       DCHECK(model_->VerifyRefCount());
179 
180       // Menu is empty so notify the client and return.
181       if (model_->GetCount() == 0 && !custom_menu) {
182         MenuClosed(model_);
183         return true;
184       }
185     }
186   }
187 
188   if (custom_menu || !runner_)
189     return true;
190   return runner_->RunContextMenu(browser_, model_.get(), params_);
191 }
192 
CancelContextMenu()193 void CefMenuManager::CancelContextMenu() {
194   if (IsShowingContextMenu()) {
195     if (custom_menu_callback_)
196       custom_menu_callback_->Cancel();
197     else if (runner_)
198       runner_->CancelContextMenu();
199   }
200 }
201 
ExecuteCommand(CefRefPtr<CefMenuModelImpl> source,int command_id,cef_event_flags_t event_flags)202 void CefMenuManager::ExecuteCommand(CefRefPtr<CefMenuModelImpl> source,
203                                     int command_id,
204                                     cef_event_flags_t event_flags) {
205   // Give the client a chance to handle the command.
206   CefRefPtr<CefClient> client = browser_->GetClient();
207   if (client.get()) {
208     CefRefPtr<CefContextMenuHandler> handler = client->GetContextMenuHandler();
209     if (handler.get()) {
210       CefRefPtr<CefContextMenuParamsImpl> paramsPtr(
211           new CefContextMenuParamsImpl(&params_));
212 
213       bool handled = handler->OnContextMenuCommand(
214           browser_, browser_->GetFocusedFrame(), paramsPtr.get(), command_id,
215           event_flags);
216 
217       // Do not keep references to the parameters in the callback.
218       paramsPtr->Detach(nullptr);
219       DCHECK(paramsPtr->HasOneRef());
220 
221       if (handled)
222         return;
223     }
224   }
225 
226   // Execute the default command handling.
227   ExecuteDefaultCommand(command_id);
228 }
229 
MenuWillShow(CefRefPtr<CefMenuModelImpl> source)230 void CefMenuManager::MenuWillShow(CefRefPtr<CefMenuModelImpl> source) {
231   // May be called for sub-menus as well.
232   if (source.get() != model_.get())
233     return;
234 
235   if (!web_contents())
236     return;
237 
238   // May be called multiple times.
239   if (IsShowingContextMenu())
240     return;
241 
242   // Notify the host before showing the context menu.
243   web_contents()->SetShowingContextMenu(true);
244 }
245 
MenuClosed(CefRefPtr<CefMenuModelImpl> source)246 void CefMenuManager::MenuClosed(CefRefPtr<CefMenuModelImpl> source) {
247   // May be called for sub-menus as well.
248   if (source.get() != model_.get())
249     return;
250 
251   if (!web_contents())
252     return;
253 
254   DCHECK(IsShowingContextMenu());
255 
256   // Notify the client.
257   CefRefPtr<CefClient> client = browser_->GetClient();
258   if (client.get()) {
259     CefRefPtr<CefContextMenuHandler> handler = client->GetContextMenuHandler();
260     if (handler.get()) {
261       handler->OnContextMenuDismissed(browser_, browser_->GetFocusedFrame());
262     }
263   }
264 
265   // Notify the host after closing the context menu.
266   web_contents()->SetShowingContextMenu(false);
267   web_contents()->NotifyContextMenuClosed(params_.link_followed);
268 }
269 
FormatLabel(CefRefPtr<CefMenuModelImpl> source,std::u16string & label)270 bool CefMenuManager::FormatLabel(CefRefPtr<CefMenuModelImpl> source,
271                                  std::u16string& label) {
272   if (!runner_)
273     return false;
274   return runner_->FormatLabel(label);
275 }
276 
ExecuteCommandCallback(int command_id,cef_event_flags_t event_flags)277 void CefMenuManager::ExecuteCommandCallback(int command_id,
278                                             cef_event_flags_t event_flags) {
279   DCHECK(IsShowingContextMenu());
280   DCHECK(custom_menu_callback_);
281   if (command_id != kInvalidCommandId)
282     ExecuteCommand(model_, command_id, event_flags);
283   MenuClosed(model_);
284   custom_menu_callback_ = nullptr;
285 }
286 
CreateDefaultModel()287 void CefMenuManager::CreateDefaultModel() {
288   if (!params_.custom_items.empty()) {
289     // Custom menu items originating from the renderer process. For example,
290     // plugin placeholder menu items.
291     for (auto& item : params_.custom_items) {
292       auto new_item = item->Clone();
293       new_item->action += MENU_ID_CUSTOM_FIRST;
294       DCHECK_LE(static_cast<int>(new_item->action), MENU_ID_CUSTOM_LAST);
295       model_->AddMenuItem(*new_item);
296     }
297     return;
298   }
299 
300   if (params_.is_editable) {
301     // Editable node.
302     model_->AddItem(MENU_ID_UNDO, GetLabel(IDS_CONTENT_CONTEXT_UNDO));
303     model_->AddItem(MENU_ID_REDO, GetLabel(IDS_CONTENT_CONTEXT_REDO));
304 
305     model_->AddSeparator();
306     model_->AddItem(MENU_ID_CUT, GetLabel(IDS_CONTENT_CONTEXT_CUT));
307     model_->AddItem(MENU_ID_COPY, GetLabel(IDS_CONTENT_CONTEXT_COPY));
308     model_->AddItem(MENU_ID_PASTE, GetLabel(IDS_CONTENT_CONTEXT_PASTE));
309 
310     model_->AddSeparator();
311     model_->AddItem(MENU_ID_SELECT_ALL,
312                     GetLabel(IDS_CONTENT_CONTEXT_SELECTALL));
313 
314     if (!(params_.edit_flags & CM_EDITFLAG_CAN_UNDO))
315       model_->SetEnabled(MENU_ID_UNDO, false);
316     if (!(params_.edit_flags & CM_EDITFLAG_CAN_REDO))
317       model_->SetEnabled(MENU_ID_REDO, false);
318     if (!(params_.edit_flags & CM_EDITFLAG_CAN_CUT))
319       model_->SetEnabled(MENU_ID_CUT, false);
320     if (!(params_.edit_flags & CM_EDITFLAG_CAN_COPY))
321       model_->SetEnabled(MENU_ID_COPY, false);
322     if (!(params_.edit_flags & CM_EDITFLAG_CAN_PASTE))
323       model_->SetEnabled(MENU_ID_PASTE, false);
324     if (!(params_.edit_flags & CM_EDITFLAG_CAN_DELETE))
325       model_->SetEnabled(MENU_ID_DELETE, false);
326     if (!(params_.edit_flags & CM_EDITFLAG_CAN_SELECT_ALL))
327       model_->SetEnabled(MENU_ID_SELECT_ALL, false);
328 
329     if (!params_.misspelled_word.empty()) {
330       // Always add a separator before the list of dictionary suggestions or
331       // "No spelling suggestions".
332       model_->AddSeparator();
333 
334       if (!params_.dictionary_suggestions.empty()) {
335         for (size_t i = 0; i < params_.dictionary_suggestions.size() &&
336                            MENU_ID_SPELLCHECK_SUGGESTION_0 + i <=
337                                MENU_ID_SPELLCHECK_SUGGESTION_LAST;
338              ++i) {
339           model_->AddItem(MENU_ID_SPELLCHECK_SUGGESTION_0 + static_cast<int>(i),
340                           params_.dictionary_suggestions[i]);
341         }
342 
343         // When there are dictionary suggestions add a separator before "Add to
344         // dictionary".
345         model_->AddSeparator();
346       } else {
347         model_->AddItem(MENU_ID_NO_SPELLING_SUGGESTIONS,
348                         GetLabel(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
349         model_->SetEnabled(MENU_ID_NO_SPELLING_SUGGESTIONS, false);
350       }
351 
352       model_->AddItem(MENU_ID_ADD_TO_DICTIONARY,
353                       GetLabel(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
354     }
355   } else if (!params_.selection_text.empty()) {
356     // Something is selected.
357     model_->AddItem(MENU_ID_COPY, GetLabel(IDS_CONTENT_CONTEXT_COPY));
358   } else if (!params_.page_url.is_empty() || !params_.frame_url.is_empty()) {
359     // Page or frame.
360     model_->AddItem(MENU_ID_BACK, GetLabel(IDS_CONTENT_CONTEXT_BACK));
361     model_->AddItem(MENU_ID_FORWARD, GetLabel(IDS_CONTENT_CONTEXT_FORWARD));
362 
363     model_->AddSeparator();
364     model_->AddItem(MENU_ID_PRINT, GetLabel(IDS_CONTENT_CONTEXT_PRINT));
365     model_->AddItem(MENU_ID_VIEW_SOURCE,
366                     GetLabel(IDS_CONTENT_CONTEXT_VIEWPAGESOURCE));
367 
368     if (!browser_->CanGoBack())
369       model_->SetEnabled(MENU_ID_BACK, false);
370     if (!browser_->CanGoForward())
371       model_->SetEnabled(MENU_ID_FORWARD, false);
372   }
373 }
374 
ExecuteDefaultCommand(int command_id)375 void CefMenuManager::ExecuteDefaultCommand(int command_id) {
376   if (IsCustomContextMenuCommand(command_id)) {
377     if (web_contents()) {
378       web_contents()->ExecuteCustomContextMenuCommand(
379           command_id - MENU_ID_CUSTOM_FIRST, params_.link_followed);
380     }
381     return;
382   }
383 
384   // If the user chose a replacement word for a misspelling, replace it here.
385   if (command_id >= MENU_ID_SPELLCHECK_SUGGESTION_0 &&
386       command_id <= MENU_ID_SPELLCHECK_SUGGESTION_LAST) {
387     const size_t suggestion_index =
388         static_cast<size_t>(command_id) - MENU_ID_SPELLCHECK_SUGGESTION_0;
389     if (suggestion_index < params_.dictionary_suggestions.size()) {
390       browser_->ReplaceMisspelling(
391           params_.dictionary_suggestions[suggestion_index]);
392     }
393     return;
394   }
395 
396   switch (command_id) {
397     // Navigation.
398     case MENU_ID_BACK:
399       browser_->GoBack();
400       break;
401     case MENU_ID_FORWARD:
402       browser_->GoForward();
403       break;
404     case MENU_ID_RELOAD:
405       browser_->Reload();
406       break;
407     case MENU_ID_RELOAD_NOCACHE:
408       browser_->ReloadIgnoreCache();
409       break;
410     case MENU_ID_STOPLOAD:
411       browser_->StopLoad();
412       break;
413 
414     // Editing.
415     case MENU_ID_UNDO:
416       browser_->GetFocusedFrame()->Undo();
417       break;
418     case MENU_ID_REDO:
419       browser_->GetFocusedFrame()->Redo();
420       break;
421     case MENU_ID_CUT:
422       browser_->GetFocusedFrame()->Cut();
423       break;
424     case MENU_ID_COPY:
425       browser_->GetFocusedFrame()->Copy();
426       break;
427     case MENU_ID_PASTE:
428       browser_->GetFocusedFrame()->Paste();
429       break;
430     case MENU_ID_DELETE:
431       browser_->GetFocusedFrame()->Delete();
432       break;
433     case MENU_ID_SELECT_ALL:
434       browser_->GetFocusedFrame()->SelectAll();
435       break;
436 
437     // Miscellaneous.
438     case MENU_ID_FIND:
439       // TODO(cef): Implement.
440       NOTIMPLEMENTED();
441       break;
442     case MENU_ID_PRINT:
443       browser_->Print();
444       break;
445     case MENU_ID_VIEW_SOURCE:
446       browser_->GetFocusedFrame()->ViewSource();
447       break;
448 
449     // Spell checking.
450     case MENU_ID_ADD_TO_DICTIONARY:
451       browser_->GetHost()->AddWordToDictionary(params_.misspelled_word);
452       break;
453 
454     default:
455       break;
456   }
457 }
458 
IsCustomContextMenuCommand(int command_id)459 bool CefMenuManager::IsCustomContextMenuCommand(int command_id) {
460   // Verify that the command ID is in the correct range.
461   if (command_id < MENU_ID_CUSTOM_FIRST || command_id > MENU_ID_CUSTOM_LAST)
462     return false;
463 
464   command_id -= MENU_ID_CUSTOM_FIRST;
465 
466   // Verify that the specific command ID was passed from the renderer process.
467   if (!params_.custom_items.empty()) {
468     for (size_t i = 0; i < params_.custom_items.size(); ++i) {
469       if (static_cast<int>(params_.custom_items[i]->action) == command_id)
470         return true;
471     }
472   }
473   return false;
474 }
475