2021-09-23 13:43:28 +10:00
|
|
|
// 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
|
|
|
|
|
{
|
2021-11-27 20:02:48 +10:00
|
|
|
/// <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;
|
|
|
|
|
|
2021-09-23 13:43:28 +10:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-27 20:02:48 +10:00
|
|
|
model.AddItem((CefMenuCommand)CefMenuCommandShowDevToolsId, "Show DevTools (Inspect)");
|
|
|
|
|
model.AddItem((CefMenuCommand)CefMenuCommandCloseDevToolsId, "Close DevTools");
|
2021-09-23 13:43:28 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
{
|
2021-11-27 20:02:48 +10:00
|
|
|
if (item.IsSeperator)
|
2021-09-23 13:43:28 +10:00
|
|
|
{
|
2021-11-27 20:02:48 +10:00
|
|
|
menu.Items.Add(new Separator());
|
|
|
|
|
|
2021-09-23 13:43:28 +10:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-27 20:02:48 +10:00
|
|
|
if (item.CommandId == CefMenuCommand.NotFound)
|
2021-09-23 13:43:28 +10:00
|
|
|
{
|
|
|
|
|
continue;
|
2021-11-27 20:02:48 +10:00
|
|
|
}
|
2021-09-23 13:43:28 +10:00
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
{
|
2022-02-22 11:17:56 +10:00
|
|
|
browser.GetHost().Find(model.SelectionText, true, false, false);
|
2021-09-23 13:43:28 +10:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spell checking.
|
|
|
|
|
case CefMenuCommand.AddToDictionary:
|
|
|
|
|
{
|
|
|
|
|
browser.GetHost().AddWordToDictionary(model.MisspelledWord);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-27 20:02:48 +10:00
|
|
|
case (CefMenuCommand)CefMenuCommandShowDevToolsId:
|
2021-09-23 13:43:28 +10:00
|
|
|
{
|
|
|
|
|
browser.GetHost().ShowDevTools(inspectElementAtX: model.XCoord, inspectElementAtY: model.YCoord);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-11-27 20:02:48 +10:00
|
|
|
case (CefMenuCommand)CefMenuCommandCloseDevToolsId:
|
2021-09-23 13:43:28 +10:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
2022-02-22 11:33:18 +10:00
|
|
|
var subMenus = subItems == null ? null : GetMenuItems(subItems);
|
2021-09-23 13:43:28 +10:00
|
|
|
|
|
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|