Thursday, June 5, 2014

Handling Popup Menu (aka Context Menu) in a Gtk# TreeView widget

There are two ways to invoke the context menu, namely the familiar right mouse click and pressing Shift-F10.

To handle these two triggers requires some special treatments and this post is to illustrate how to accomplish them. The Internet and in fact the Mono forums contain materials showing how to handle the right mouse click and it is repeated here for completeness but seem to fail to discuss how to deal with Shift-F10.

To intercept the right mouse click

To intercept the right mouse click you need to provide a ButtonPress event handler to the TreeView's ButtonPress event and most importantly you need to tell the runtime to slot your handler before the default ButtonPress handler, or else you won't see the right mouse click.

Here is the code fragment to add a ButtonPress event handler, which you can also specify using the properties pad in MonoDevelopment IDE.

this.treeview1.PopupMenu += new global::Gtk.PopupMenuHandler (this.OnTreeview1PopupMenu)

When defining the handler, make sure you adore the handler with the Glib.ConnectBefore attribute as follows:

[ GLib.ConnectBefore ] // need this to allow program to intercept the key first.
protected void OnTreeview1ButtonPressEvent (object o, ButtonPressEventArgs args)
{
    if( args.Event.Button == 3 )    // Right button click
    {
        Debug.WriteLine( "Right mouse click detected" );
        ShowContextMenu("Right Click - ");
    }
}

To handle Shift-F10

It seems the treatment to invoke the context menu via the keystroke is not as popular as using the right mouse click and hence the forums have scant discussion on this. This may be due to poor documentation, in particular to the use of PopupMenuArgs, the event argument for the PopupMenuHandler delegate. In the absence of a recommended way from the Mono team to invoke the pop up, below offers a possible way to accomplish this.

When Shift-F10 is pressed on a TreeView, the runtime will invoke the TreeView.PopupMenu event handler if defined. This is different from other widget's way of handling context menu; Others have a PopulatePopupMenu event allowing the control's provider to construct the menu and the runtime will then render the menu.

Hooking the TreeView.PopupMenu event is easy but the same treatment used in invoking the user constructed pop up menu for right mouse click does not seem to work; in fact nothing is displayed. Hence it needs a slight departure from simply calling directly the Menu.Popup in the TreeView.PopupMenu event handler. You need to basically post yourself a message, using a Windows' parlance, and then you can invoke the popup. This can be accomplished by using the Application.Invoke method or its overloads.

Here is the code fragment to install an event handler for PopupMenu:

this.treeview1.ButtonPressEvent += new global::Gtk.ButtonPressEventHandler (this.OnTreeview1ButtonPressEvent);

Once again if you prefer, you can use the properties pad for the TreeView object in the MonoDevelop to specify this.

Below is the code fragment showing the way to use Application.Invoke() to show the context menu:

protected void OnTreeview1PopupMenu (object o, PopupMenuArgs args)
{
    Debug.WriteLine( "TreeView Popup Menu click" );
    // Post yourself a message to invoke the context menu.
    // Calling ShowContextMenu() directly from here does not
    // work.
    Application.Invoke( delegate 
        {
            Debug.WriteLine( "Delegate invoked" );
            ShowContextMenu("Shift-F10 - ");
        } );
}

For completeness, here is the implementation of very simple ShowContextMenu():

void ShowContextMenu (String prefix = "")
{
    Menu m = new Menu();
    MenuItem mi = new MenuItem( prefix + "Item 1" );
    mi.Activated += (object sender, EventArgs e) => 
    {
        String s = prefix + "Item 1 Clicked";
        Debug.WriteLine( s );
        ShowMsgBox( this, s ); 
    };
    m.Append(mi);
    
    mi = new MenuItem( prefix + "Item 2" );
    mi.Activated += (object sender, EventArgs e) => 
    {
        String s = prefix + "Item 2 Clicked";
        Debug.WriteLine( s );
        ShowMsgBox( this, s );
    };
    m.Append( mi ); 
 
    m.ShowAll();
    m.Popup();
}