You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
340 lines
12 KiB
340 lines
12 KiB
// Copyright © 2021 The CefSharp Authors. All rights reserved. |
|
// |
|
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. |
|
|
|
using System.Collections.Generic; |
|
using System.Windows.Controls.Primitives; |
|
using System.Windows.Controls; |
|
using System.Windows; |
|
|
|
namespace CefSharp.Wpf.Handler |
|
{ |
|
/// <summary> |
|
/// Implementation of <see cref="IContextMenuHandler"/> that uses a <see cref="ContextMenu"/> |
|
/// to display the context menu. |
|
/// </summary> |
|
public class ContextMenuHandler : CefSharp.Handler.ContextMenuHandler |
|
{ |
|
/// <summary> |
|
/// Open DevTools <see cref="CefMenuCommand"/> Id |
|
/// </summary> |
|
public const int CefMenuCommandShowDevToolsId = 28440; |
|
/// <summary> |
|
/// Close DevTools <see cref="CefMenuCommand"/> Id |
|
/// </summary> |
|
public const int CefMenuCommandCloseDevToolsId = 28441; |
|
|
|
private readonly bool addDevtoolsMenuItems; |
|
|
|
public ContextMenuHandler(bool addDevtoolsMenuItems = false) |
|
{ |
|
this.addDevtoolsMenuItems = addDevtoolsMenuItems; |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnBeforeContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) |
|
{ |
|
if (addDevtoolsMenuItems) |
|
{ |
|
if (model.Count > 0) |
|
{ |
|
model.AddSeparator(); |
|
} |
|
|
|
model.AddItem((CefMenuCommand)CefMenuCommandShowDevToolsId, "Show DevTools (Inspect)"); |
|
model.AddItem((CefMenuCommand)CefMenuCommandCloseDevToolsId, "Close DevTools"); |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnContextMenuDismissed(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame) |
|
{ |
|
var webBrowser = (ChromiumWebBrowser)chromiumWebBrowser; |
|
|
|
webBrowser.UiThreadRunAsync(() => |
|
{ |
|
webBrowser.ContextMenu = null; |
|
}); |
|
} |
|
|
|
|
|
/// <inheritdoc/> |
|
protected override bool RunContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) |
|
{ |
|
var webBrowser = (ChromiumWebBrowser)chromiumWebBrowser; |
|
|
|
//IMenuModel is only valid in the context of this method, so need to read the values before invoking on the UI thread |
|
var menuItems = GetMenuItems(model); |
|
var dictionarySuggestions = parameters.DictionarySuggestions; |
|
var xCoord = parameters.XCoord; |
|
var yCoord = parameters.YCoord; |
|
var misspelledWord = parameters.MisspelledWord; |
|
var selectionText = parameters.SelectionText; |
|
|
|
webBrowser.UiThreadRunAsync(() => |
|
{ |
|
var menu = new ContextMenu |
|
{ |
|
IsOpen = true, |
|
Placement = PlacementMode.Mouse |
|
}; |
|
|
|
RoutedEventHandler handler = null; |
|
|
|
handler = (s, e) => |
|
{ |
|
menu.Closed -= handler; |
|
|
|
//If the callback has been disposed then it's already been executed |
|
//so don't call Cancel |
|
if (!callback.IsDisposed) |
|
{ |
|
callback.Cancel(); |
|
} |
|
}; |
|
|
|
menu.Closed += handler; |
|
|
|
foreach (var item in menuItems) |
|
{ |
|
if (item.IsSeperator) |
|
{ |
|
menu.Items.Add(new Separator()); |
|
|
|
continue; |
|
} |
|
|
|
if (item.CommandId == CefMenuCommand.NotFound) |
|
{ |
|
continue; |
|
} |
|
|
|
var menuItem = new MenuItem |
|
{ |
|
Header = item.Label.Replace("&", "_"), |
|
IsEnabled = item.IsEnabled, |
|
IsChecked = item.IsChecked.GetValueOrDefault(), |
|
IsCheckable = item.IsChecked.HasValue, |
|
Command = new DelegateCommand(() => |
|
{ |
|
//BUG: CEF currently not executing callbacks correctly so we manually map the commands below |
|
//see https://github.com/cefsharp/CefSharp/issues/1767 |
|
//The following line worked in previous versions, it doesn't now, so custom handling below |
|
//callback.Continue(item.Item2, CefEventFlags.None); |
|
ExecuteCommand(browser, new ContextMenuExecuteModel(item.CommandId, dictionarySuggestions, xCoord, yCoord, selectionText, misspelledWord)); |
|
}), |
|
}; |
|
|
|
//TODO: Make this recursive and remove duplicate code |
|
if(item.SubMenus != null && item.SubMenus.Count > 0) |
|
{ |
|
foreach(var subItem in item.SubMenus) |
|
{ |
|
if (subItem.CommandId == CefMenuCommand.NotFound) |
|
{ |
|
continue; |
|
} |
|
|
|
if (subItem.IsSeperator) |
|
{ |
|
menu.Items.Add(new Separator()); |
|
|
|
continue; |
|
} |
|
|
|
var subMenuItem = new MenuItem |
|
{ |
|
Header = subItem.Label.Replace("&", "_"), |
|
IsEnabled = subItem.IsEnabled, |
|
IsChecked = subItem.IsChecked.GetValueOrDefault(), |
|
IsCheckable = subItem.IsChecked.HasValue, |
|
Command = new DelegateCommand(() => |
|
{ |
|
//BUG: CEF currently not executing callbacks correctly so we manually map the commands below |
|
//see https://github.com/cefsharp/CefSharp/issues/1767 |
|
//The following line worked in previous versions, it doesn't now, so custom handling below |
|
//callback.Continue(item.Item2, CefEventFlags.None); |
|
ExecuteCommand(browser, new ContextMenuExecuteModel(subItem.CommandId, dictionarySuggestions, xCoord, yCoord, selectionText, misspelledWord)); |
|
}), |
|
}; |
|
|
|
menuItem.Items.Add(subMenuItem); |
|
} |
|
} |
|
|
|
menu.Items.Add(menuItem); |
|
} |
|
webBrowser.ContextMenu = menu; |
|
}); |
|
|
|
return true; |
|
} |
|
|
|
protected virtual void ExecuteCommand(IBrowser browser, ContextMenuExecuteModel model) |
|
{ |
|
// If the user chose a replacement word for a misspelling, replace it here. |
|
if (model.MenuCommand >= CefMenuCommand.SpellCheckSuggestion0 && |
|
model.MenuCommand <= CefMenuCommand.SpellCheckSuggestion4) |
|
{ |
|
int sugestionIndex = ((int)model.MenuCommand) - (int)CefMenuCommand.SpellCheckSuggestion0; |
|
if (sugestionIndex < model.DictionarySuggestions.Count) |
|
{ |
|
var suggestion = model.DictionarySuggestions[sugestionIndex]; |
|
browser.ReplaceMisspelling(suggestion); |
|
} |
|
|
|
return; |
|
} |
|
|
|
switch (model.MenuCommand) |
|
{ |
|
// Navigation. |
|
case CefMenuCommand.Back: |
|
{ |
|
browser.GoBack(); |
|
break; |
|
} |
|
case CefMenuCommand.Forward: |
|
{ |
|
browser.GoForward(); |
|
break; |
|
} |
|
case CefMenuCommand.Reload: |
|
{ |
|
browser.Reload(); |
|
break; |
|
} |
|
case CefMenuCommand.ReloadNoCache: |
|
{ |
|
browser.Reload(ignoreCache: true); |
|
break; |
|
} |
|
case CefMenuCommand.StopLoad: |
|
{ |
|
browser.StopLoad(); |
|
break; |
|
} |
|
|
|
//Editing |
|
case CefMenuCommand.Undo: |
|
{ |
|
browser.FocusedFrame.Undo(); |
|
break; |
|
} |
|
case CefMenuCommand.Redo: |
|
{ |
|
browser.FocusedFrame.Redo(); |
|
break; |
|
} |
|
case CefMenuCommand.Cut: |
|
{ |
|
browser.FocusedFrame.Cut(); |
|
break; |
|
} |
|
case CefMenuCommand.Copy: |
|
{ |
|
browser.FocusedFrame.Copy(); |
|
break; |
|
} |
|
case CefMenuCommand.Paste: |
|
{ |
|
browser.FocusedFrame.Paste(); |
|
break; |
|
} |
|
case CefMenuCommand.Delete: |
|
{ |
|
browser.FocusedFrame.Delete(); |
|
break; |
|
} |
|
case CefMenuCommand.SelectAll: |
|
{ |
|
browser.FocusedFrame.SelectAll(); |
|
break; |
|
} |
|
|
|
// Miscellaneous. |
|
case CefMenuCommand.Print: |
|
{ |
|
browser.GetHost().Print(); |
|
break; |
|
} |
|
case CefMenuCommand.ViewSource: |
|
{ |
|
browser.FocusedFrame.ViewSource(); |
|
break; |
|
} |
|
case CefMenuCommand.Find: |
|
{ |
|
browser.GetHost().Find(model.SelectionText, true, false, false); |
|
break; |
|
} |
|
|
|
// Spell checking. |
|
case CefMenuCommand.AddToDictionary: |
|
{ |
|
browser.GetHost().AddWordToDictionary(model.MisspelledWord); |
|
break; |
|
} |
|
|
|
case (CefMenuCommand)CefMenuCommandShowDevToolsId: |
|
{ |
|
browser.GetHost().ShowDevTools(inspectElementAtX: model.XCoord, inspectElementAtY: model.YCoord); |
|
break; |
|
} |
|
case (CefMenuCommand)CefMenuCommandCloseDevToolsId: |
|
{ |
|
browser.GetHost().CloseDevTools(); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
private static IList<MenuModel> GetMenuItems(IMenuModel model) |
|
{ |
|
var menuItems = new List<MenuModel>(); |
|
|
|
for (var i = 0; i < model.Count; i++) |
|
{ |
|
var type = model.GetTypeAt(i); |
|
bool? isChecked = null; |
|
|
|
if(type == MenuItemType.Check) |
|
{ |
|
isChecked = model.IsCheckedAt(i); |
|
} |
|
|
|
var subItems = model.GetSubMenuAt(i); |
|
|
|
var subMenus = subItems == null ? null : GetMenuItems(subItems); |
|
|
|
var menuItem = new MenuModel |
|
{ |
|
Label = model.GetLabelAt(i), |
|
CommandId = model.GetCommandIdAt(i), |
|
IsEnabled = model.IsEnabledAt(i), |
|
Type = type, |
|
IsSeperator = type == MenuItemType.Separator, |
|
IsChecked = isChecked, |
|
SubMenus = subMenus |
|
}; |
|
|
|
menuItems.Add(menuItem); |
|
} |
|
|
|
return menuItems; |
|
} |
|
|
|
//TODO: One class per file |
|
internal class MenuModel |
|
{ |
|
internal string Label { get; set; } |
|
internal CefMenuCommand CommandId { get; set; } |
|
internal bool IsEnabled { get; set; } |
|
internal bool IsSeperator { get; set; } |
|
internal bool? IsChecked { get; set; } |
|
internal MenuItemType Type { get; set; } |
|
|
|
internal IList<MenuModel> SubMenus { get; set; } |
|
} |
|
} |
|
}
|
|
|