Right-Click Context Menus In GtkSharp

Published on Monday, April 12, 2010

To show a context menu in GtkSharp (or "popup" as they're called in Gtk land), you would normally add an event handler for Widget.PopupMenu, create or use a Menu instance, and then call Menu.Popup. The only problem is that for many widgets, the right-click doesn't trigger the Widget.PopupMenu event. This is fine for systems where there is no right mouse button or where a right-click isn't the customary way of initiating context menus. However, on systems where there is a user expectation that the way to open a context menu is through a right-click (such as Windows), we need some way to trigger one.

The situation is complicated a little bit by the existing behavior of Widget.PopupMenu. According to the Gtk manual, "By default, the key binding mechanism is set to emit this signal when the Shift+F10 or Menu keys are pressed while a widget has the focus." There is a recommendation in the manual that if a developer wants context menus on right-click they should handle the Widget.ButtonPressEvent, listen for the appropriate clicks, and launch the menu using the Menu.Popup method. This is all fine except that now you've got two things to listen to to get proper context menu handling: Widget.PopupMenu and Widget.ButtonPressEvent. It would be nice if there were just one event to handle that got raised anytime a context menu needed to be displayed.

The following class does just that. You can "attach" it to any Widget and it will listen for the Widget.PopupMenu event to work with the default context menu handling and the Widget.ButtonPressEvent to also work with right-clicks. When either of these occur, it will first propagate the event through to the underlying Widget (in case there are other things that are supposed to be triggered by whatever event caused the context menu) and then raise a ContextMenuHelper.ContextMenu event that you can handle and use to display the context menu regardless of what triggered it. This method should ensure proper event handling and ordering while reducing duplication of code by enabling a single event for context menu handling.

using System;
using Gdk;
using GLib;
using Gtk;

namespace DaveAGlick
{
 public class ContextMenuEventArgs : EventArgs
 {
  private Widget widget;
  public Widget Widget { get { return widget; } }

  private bool rightClick;
  public bool RightClick { get { return rightClick; } }

  public ContextMenuEventArgs(Widget widget, bool rightClick)
  {
   this.widget = widget;
   this.rightClick = rightClick;
  }
 }

 public class ContextMenuHelper
 {
  public event EventHandler<ContextMenuEventArgs> ContextMenu;

  public ContextMenuHelper()
  {}

  public ContextMenuHelper(Widget widget)
  {
   AttachToWidget(widget);
  }

  public ContextMenuHelper(Widget widget, EventHandler handler)
  {
   AttachToWidget(widget);
   ContextMenu += handler;
  }

  public void AttachToWidget(Widget widget)
  {
   widget.PopupMenu += Widget_PopupMenu;
   widget.ButtonPressEvent += Widget_ButtonPressEvent;
  }

  public void DetachFromWidget(Widget widget)
  {
   widget.PopupMenu -= Widget_PopupMenu;
   widget.ButtonPressEvent -= Widget_ButtonPressEvent;
  }

  [GLib.ConnectBefore]
  private void Widget_PopupMenu(object o, PopupMenuArgs args)
  {
   RaiseContextMenuEvent(args, (Widget)o, false);
  }

  [GLib.ConnectBefore]
  private void Widget_ButtonPressEvent(object o, ButtonPressEventArgs args)
  {
   if (args.Event.Button == 3 && args.Event.Type == EventType.ButtonPress)
   {
    RaiseContextMenuEvent(args, (Widget)o, true);
   }
  }

  private bool propagating = false;   //Prevent reentry

  private void RaiseContextMenuEvent(SignalArgs signalArgs, Widget widget, bool rightClick)
  {
   if (!propagating)
   {
    //Propagate the event
    Event evnt = Gtk.Global.CurrentEvent;
    propagating = true;
    Gtk.Global.PropagateEvent(widget, evnt);
    propagating = false;
    signalArgs.RetVal = true;     //The widget already processed the event in the propagation

    //Raise the context menu event
    ContextMenuEventArgs args = new ContextMenuEventArgs(widget, rightClick);
    if (ContextMenu != null)
    {
     ContextMenu.Invoke(this, args);
    }
   }
  }
 }
}