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