WebView2Feedback icon indicating copy to clipboard operation
WebView2Feedback copied to clipboard

Customize context menus + Past Data

Open leonidukg opened this issue 3 years ago • 13 comments

Hi all.

C#/WPF/NET6

The task is to insert data into an IsEditable field on a website WITHOUT using JavaScript.

CoreWebView2ContextMenuTarget has a great IsEditable check. But what to do with it next, there is no documentation. No information on how to insert the data.

These are all very new functions and there is very little experience with them all. That's why I hope to be able to help me here.

AB#42679943

leonidukg avatar Dec 11 '22 08:12 leonidukg

Maybe the question is rather complicated or something else has not been implemented in the API.

Let's look at the other side, how can you call the Paste command from the clipboard for your menu item? Not just rename an existing item, but call the command.

leonidukg avatar Dec 16 '22 07:12 leonidukg

Hi @leonidukg , you can set the command ID for Paste to SelectedCommandId to run the Paste command on a ContextMenuRequested event. You can use the IsEditable field to check that the context menu target is indeed editable. Hope this helps!

yildirimcagri-msft avatar Dec 16 '22 12:12 yildirimcagri-msft

My task without code is not quite clear. But I gave you an example of what works now WITHOUT javascript

The question remains whether the data can be inserted WITHOUT the clipboard. Give the command directly to Webview2 or somehow call the "Paste" command here.

if (args.ContextMenuTarget.IsEditable) //Check if the data can be inserted { if (newItemIsert == null) // Check if the menu was not created earlier { CoreWebView2ContextMenuItem submenu; // Add a main menu item to which we will add a submenu newItemIsert = WebView.CoreWebView2.Environment.CreateContextMenuItem("Insert my DATA", Application.GetResourceStream(new Uri("pack://application:,,,/Images/ASRlogo.ico")).Stream, CoreWebView2ContextMenuItemKind.Submenu); //Create a submenu from our list foreach (var list in GetInputList()) { submenu.CustomItemSelected += (s, ex) => {

                        System.Threading.SynchronizationContext.Current.Post((_) =>
                        {
			// And this is where we begin to have questions
			// Now the paste is created via the clipboard. With a delay because the emulation sometimes works too fast and the buffer is empty.
                            string out_text = TextInfo(list.Key, list.Value);
                            Clipboard.SetText(out_text);
                            System.Threading.Thread.Sleep(300);

                            new InputSimulator().Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_V);

                        }, null);

                    };
                    newItemIsert.Children.Add(submenu);
                }
        }

leonidukg avatar Dec 16 '22 13:12 leonidukg

You don't need to create a new custom item to induce the command. However, you do need to set the data on clipboard as the Paste command will take the value from clipboard.

if (args.ContextMenuTarget.IsEditable) //Check if the data can be inserted
{
   // Set text on clipboard
   Clipboard.SetText(out_text);
   
   // Set the paste command ID as selected so "Paste" command is run (pastes data from clipboard).
    foreach (CoreWebView2ContextMenuItem item in args.MenuItems) {
        if (item.Name == "paste") {
             args.SelectedCommandId = item.CommandId;
        }
    }
}

If you'd like to avoid to set the data to clipboard via app code, you may also be able to set it directly from JavaScript via Clipboard API, however for this one you also need to grant the Clipboard permission via the WebView2 Permissions API.

If you are trying to create another item, then you can just set the CommandId of the new item you created to be the "paste" command.

if (args.ContextMenuTarget.IsEditable) //Check if the data can be inserted
{
   newItemIsert = WebView.CoreWebView2.Environment.CreateContextMenuItem("Insert my DATA", Application.GetResourceStream(new Uri("pack://application:,,,/Images/ASRlogo.ico")).Stream, CoreWebView2ContextMenuItemKind.Submenu);
   
   // Set the paste command ID as selected so "Paste" command is run (pastes data from clipboard).
    foreach (CoreWebView2ContextMenuItem item in args.MenuItems) {
        if (item.Name == "paste") {
             newItemIsert.CommandId = item.CommandId;
        }
    }
// Insert new item in context menu
   ...
}

yildirimcagri-msft avatar Dec 16 '22 13:12 yildirimcagri-msft

newItemIsert.CommandId = item.CommandId;

Error, newItemIsert.CommandId readonly

leonidukg avatar Dec 16 '22 14:12 leonidukg

Sorry, I guess we didn't allow setting the command ID for custom items. In this case you'd use the CustomItemSelected event to set the SelectedCommandId property.

newItemInsert = WebView.CoreWebView2.Environment.CreateContextMenuItem("Insert my DATA", Application.GetResourceStream(new Uri("pack://application:,,,/Images/ASRlogo.ico")).Stream, CoreWebView2ContextMenuItemKind.Submenu);
int pasteCommandid;
CoreWebView2Deferral d;
   
...
// In ContextMenuRequested event handler
   // Save the paste command ID (only need to do this once)
  foreach (CoreWebView2ContextMenuItem item in args.MenuItems) {
        if (item.Name == "paste") {
             pasteCommandid = item.CommandId;
        }
    }
d = args.GetDeferral();
newItemInsert.CustomItemSelected += (s, ex) =>
{
  args.SelectedCommandId = pasteCommandId;
   d.Complete();
}

yildirimcagri-msft avatar Dec 16 '22 14:12 yildirimcagri-msft

Error: int pasteCommandid; Need: int pasteCommandid=-1;

The first call to the item does not give anything, and the second gives an error:: d.Complete();

System.Runtime.InteropServices.COMException: "Method called at unexpected time. (0x8000000E)"

leonidukg avatar Dec 16 '22 14:12 leonidukg

I understand your logic, I too tried to do all this a month ago. But nothing will work because of this rule:

https://learn.microsoft.com/ru-ru/dotnet/api/microsoft.web.webview2.core.corewebview2contextmenurequestedeventargs.selectedcommandid?view=webview2-dotnet-1.0.1462.37

however while command IDs for each custom context menu item is unique during a ContextMenuRequested event

leonidukg avatar Dec 16 '22 14:12 leonidukg

Sorry, in order to respond quickly I was writing some POC code :) If you can give me some time, I can try validating it. Setting the selectedCommandId to paste via the CustomItemSelected event should definitely work though as it will induce the Paste command.

yildirimcagri-msft avatar Dec 16 '22 14:12 yildirimcagri-msft

Thank you. I would not be the only one who would be very grateful if you could show me working code for such a task. Maybe it could even be inserted into the documentation, so that people understand more about what their options are.

leonidukg avatar Dec 16 '22 14:12 leonidukg

I may sound annoying, but have you been unable to write working code about using CommandId?

leonidukg avatar Dec 19 '22 15:12 leonidukg

Hi @leonidukg , it seems I have been mistaken here, apologies for this. In a given ContextMenuRequested event the SelectedCommandId and customItemSelected events are mutually exclusive, as to set the SelectedCommandId, the app needs to draw the context menu itself. I'm going to go ahead and file a feature request to see if we can improve this behavior (ideally, one should be able to set a command ID of an existing context menu item for the custom items). I have the following sample for you, which generates a second context menu event by sending a mouse input to the app when the custom item is selected, which then automatically chooses the "paste" command. You may be able to improve upon this by using SendMouseInput API instead of mouse_event, which requires the cursor to be moved as I've seen some funkiness with the mouse_event API. Sorry for not being able to come up with anything better at the moment, but again this will be on our radar for improvement.

        private CoreWebView2ContextMenuItem paste = null;
        CoreWebView2Deferral contextMenuDeferral;
        System.Drawing.Point location;
        int pasteCommandId = -1;
        bool generatedContextMenuEvent = false;
        // <CustomContextMenu>
        void WebView_ContextMenuRequested(
              object sender,
              CoreWebView2ContextMenuRequestedEventArgs args)
        {
            IList<CoreWebView2ContextMenuItem> menuList = args.MenuItems;

            CoreWebView2ContextMenuTargetKind context = args.ContextMenuTarget.Kind;

           // Choose the paste option in generated context menu event.
            if (generatedContextMenuEvent)
            {
                args.Handled = true;
                args.SelectedCommandId = pasteCommandId;
                generatedContextMenuEvent = false;
                return;
            }
            // Using custom context menu UI
            if (args.ContextMenuTarget.IsEditable)
            {
                location = args.Location;
                contextMenuDeferral = args.GetDeferral();
                if (paste == null)
                {
                    // Save the paste command ID (only need to do this once)
                    foreach (CoreWebView2ContextMenuItem item in args.MenuItems)
                    {
                        if (item.Name == "paste")
                        {
                            pasteCommandId = item.CommandId;
                        }
                    }

                    paste = webView.CoreWebView2.Environment.CreateContextMenuItem(
                              "paste", null,
                              CoreWebView2ContextMenuItemKind.Command);

                    paste.CustomItemSelected += (s, ex) =>
                    {
                        generatedContextMenuEvent = true;
                        Point p = webView.PointToScreen(new System.Windows.Point(location.X, location.Y));

                       // Generate another context menu event at the same location and choose the paste option 
                        System.Threading.SynchronizationContext.Current.Post(async (_) =>
                        {
                            SetCursorPos((int)p.X, (int)p.Y);
                            mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_RIGHTDOWN, (uint)p.X, (uint)p.Y, 0, 0);
                            mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_RIGHTUP, (uint)p.X, (uint)p.Y, 0, 0);
                        }, null);
                    };
                }
                args.MenuItems.Add(paste);
                contextMenuDeferral.Complete();
            }
        }

yildirimcagri-msft avatar Dec 20 '22 01:12 yildirimcagri-msft

Thank you for making a request for extensions of these functions.

From your code, I see that my implementation of the Ctrl+V keystroke emulation doesn't look too bad :)

leonidukg avatar Dec 20 '22 06:12 leonidukg