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(¶ms_));
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(¶ms_));
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