vbAccelerator - Contents of code file: MouseGestureCS_MouseGestureFilter.cs

using System;
using System.Collections;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace vbAccelerator.Components.Win32
{

   /// <summary>
   /// Enumerated flag values for the mouse gestures supported by 
   /// the MouseGesture class.
   /// </summary>
   [FlagsAttribute()]
   public enum MouseGestureTypes : int
   {
      /// <summary>
      /// No mouse gesture.
      /// </summary>
      NoGesture = 0x0,
      /// <summary>
      /// Mouse Gesture move north
      /// </summary>
      NorthGesture = 0x1,
      /// <summary>
      /// Mouse Gesture move south
      /// </summary>
      SouthGesture = 0x2,
      /// <summary>
      /// Mouse Gesture move east
      /// </summary>
      EastGesture = 0x4,
      /// <summary>
      /// Mouse Gesture move west
      /// </summary>
      WestGesture = 0x8,
      /// <summary>
      /// Mouse Gesture move north-east
      /// </summary>
      NorthThenEastGesture = 0x10,
      /// <summary>
      /// Mouse Gesture move south-east
      /// </summary>
      SouthThenEastGesture = 0x20,
      /// <summary>
      /// Mouse Gesture move south-west
      /// </summary>
      SouthThenWestGesture = 0x40,
      /// <summary>
      /// Mouse Gesture move north-west
      /// </summary>      
      NorthThenWestGesture = 0x80,
      /// <summary>
      /// Mouse Gesture move north-east
      /// </summary>
      EastThenNorthGesture = 0x100,
      /// <summary>
      /// Mouse Gesture move south-east
      /// </summary>
      EastThenSouthGesture = 0x200,
      /// <summary>
      /// Mouse Gesture move south-west
      /// </summary>
      WestThenSouthGesture = 0x400,
      /// <summary>
      /// Mouse Gesture move north-west
      /// </summary>      
      WestThenNorthGesture = 0x800,
      /// <summary>
      /// All mouse gestures
      /// </summary>
      AllGestureTypes = 0xFFF
   }

   /// <summary>
   /// Holds the arguments for a gesture event.  The <c>acceptGesture</c>
   /// property is used to tell the class which raises the message whether
   /// the consuming application acknowledged the gesture and therefore to 
   /// cancel the right mouse up event.
   /// </summary>
   public class MouseGestureEventArgs : EventArgs
   {
      private MouseGestureTypes gestureType;
      private Point gestureStartPosition;
      private Point gestureEndPosition;
      private bool acceptGesture;

      /// <summary>
      /// Gets the gesture type.
      /// </summary>
      public MouseGestureTypes GestureType
      {
         get
         {
            return this.gestureType;
         }
      }

      /// <summary>
      /// Gets the mouse location for the point at which the gesture
      /// was started, relative to the screen.
      /// </summary>
      public Point GestureStartPosition
      {
         get
         {
            return this.gestureStartPosition;
         }
      }

      /// <summary>
      /// Gets the mouse location for the point at which the gesture
      /// was ended, relative to the screen.
      /// </summary>
      public Point GestureEndPosition
      {
         get
         {
            return this.gestureEndPosition;
         }
      }

      /// <summary>
      /// Gets/sets whether the gesture has been processed by the 
      /// application.  By default, gestures are presumed to be unaccepted,
      /// in which case the standard right mouse up behaviour will be 
      /// activated.  By setting this property to <c>true</c> the right
      /// mouse up is filtered and the application can process the gesture.
      /// </summary>
      public bool AcceptGesture
      {
         get
         {
            return this.acceptGesture;
         }
         set
         {
            this.acceptGesture = value;
         }
      }

      /// <summary>
      /// Constructor
      /// </summary>
      /// <param name="gestureType">Type of gesture which was detected</param>
      /// <param name="gestureStartPosition">Position of mouse relative to
       screen when gesture
      /// was started</param>
      /// <param name="gestureEndPosition">Position of mouse relative to screen
       when gesture
      /// was completed</param>
      public MouseGestureEventArgs(
            MouseGestureTypes gestureType,
            Point gestureStartPosition,
            Point gestureEndPosition
         )
      {
         this.gestureType = gestureType;
         this.gestureStartPosition = gestureStartPosition;
         this.gestureEndPosition = gestureEndPosition;
         this.acceptGesture = false;
      }
   }

   /// <summary>
   /// Represents the method which handles the <c>MouseGesture</c> event
   /// raised by the <c>MouseGestureFilter</c> class.
   /// </summary>
   public delegate void MouseGestureEventHandler(object sender,
    MouseGestureEventArgs args);

   /// <summary>
   /// A Windows Message Loop filter which enables mouse gestures to 
   /// be detected over any control or window.
   /// </summary>
   /// <remarks>Controls which perform processing on Right Mouse
   /// Down (rather than the standard Right Mouse Up) will still
   /// perform the right mouse action regardless of whether a gesture
   /// is made.</remarks>
   public class MouseGestureFilter : IMessageFilter
   {
      /// <summary>
      /// 
      /// </summary>
      public event MouseGestureEventHandler MouseGesture;

      [DllImport("user32", CharSet = CharSet.Auto)]
      private extern static int PostMessage (
         IntPtr hwnd, 
         int wMsg, 
         int wParam, 
         int lParam);

      private const int WM_ACTIVATE = 0x6;
      private const int WM_RBUTTONDOWN = 0x204;
      private const int WM_MOUSEMOVE = 0x200;
      private const int WM_RBUTTONUP = 0x205;

      /// <summary>
      /// The default absolute number of pixels the mouse must travel
      /// in any direction for the gesture to be acknowledged.
      /// </summary>
      private const int DEFAULT_HYSTERESIS_PIXELS = 8;

      /// <summary>
      /// How far does the mouse have to move before it is 
      /// interpreted as a gesture?
      /// </summary>
      protected int hysteresis = DEFAULT_HYSTERESIS_PIXELS;
      /// <summary>
      /// The configured mouse gesture types
      /// </summary>
      private MouseGestureTypes gestureTypes = MouseGestureTypes.NoGesture;
      /// <summary>
      /// Whether we are checking for a gesture or not.
      /// </summary>
      private bool checkingGesture = false;
      /// <summary>
      /// The recorded mouse gesture during gesture checking
      /// </summary>
      private MouseGestureTypes recordedGesture = MouseGestureTypes.NoGesture;
      /// <summary>
      /// <c>ArrayList</c> of mouse points recorded during gesture.
      /// </summary>
      private ArrayList gesture = null;
      
      
      /// <summary>
      /// Gets/sets the mouse gesture types to look for.
      /// </summary>
      public MouseGestureTypes GestureTypes
      {
         get
         {
            return this.gestureTypes;
         }
         set
         {
            this.gestureTypes = value;
         }
      }

      
      /// <summary>
      /// Prefilters all application messages to check whether
      /// the message is a gesture or not.
      /// </summary>
      /// <param name="m">The Windows message to prefilter</param>
      /// <returns><c>true</c> if the message should be filtered (was a 
      /// processed gesture), <c>false</c> otherwise.</returns>
      public bool PreFilterMessage(
         ref Message m
         )
      {
         bool retValue = false;

         if (this.gestureTypes > 0)
         {
            if (this.checkingGesture)
            {
               if (m.Msg == WM_MOUSEMOVE)
               {
                  AddToMouseGesture();
               }
               else if (m.Msg == WM_RBUTTONUP)
               {
                  retValue = EndMouseGesture();
                  if (retValue)
                  {
                     // Windows will skip the next mouse down if we consume
                     // a mouse up.  m cannot be modified, despite being byref,
                     // so post a new one to a location which is offscreen:
                     int offScreen = 0x7fff7fff;
                     PostMessage(m.HWnd, WM_RBUTTONUP, (int)m.WParam,
                      offScreen);
                  }
               }
               else if (m.Msg == WM_ACTIVATE)
               {
                  this.checkingGesture = false;            
               }
            }
            else if (m.Msg == WM_RBUTTONDOWN)
            {
               BeginMouseGesture();
            }
         }
         return retValue;
      }

      /// <summary>
      /// 
      /// </summary>
      private void BeginMouseGesture()
      {
         gesture = new ArrayList();
         gesture.Add(Cursor.Position);
         this.checkingGesture = true;
      }

      /// <summary>
      /// 
      /// </summary>
      private void AddToMouseGesture()
      {
         gesture.Add(Cursor.Position);
      }

      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      private bool EndMouseGesture()
      {
         this.checkingGesture = false;

         bool retValue = false;

         // add the end point:
         gesture.Add(Cursor.Position);

         // get start and end:
         Point first = (Point) gesture[0];
         Point last = (Point) gesture[gesture.Count - 1];

         // check which directions we register a change in:
         int xDiff = first.X - last.X;
         int yDiff = first.Y - last.Y;

         bool north, south, east, west;
         north = south = east = west = false;

         if (Math.Abs(yDiff) > DEFAULT_HYSTERESIS_PIXELS)
         {
            north = (yDiff > 0);
            south = !north;
         }
         if (Math.Abs(xDiff) > DEFAULT_HYSTERESIS_PIXELS)
         {
            west = (xDiff > 0);
            east = !west;
         }
         // check for very narrow angles as these are probably not compound
          gestures
         if ((north || south) && (east || west))
         {
            if (Math.Abs(xDiff) > Math.Abs(yDiff))
            {
               if ((Math.Abs(xDiff) / (Math.Abs(yDiff) * 1.0)) > 7.0)
               {
                  north = south = false;
               }
            }
            else
            {
               if ((Math.Abs(yDiff) / (Math.Abs(xDiff) * 1.0)) > 7.0)
               {
                  east = west = false;
               }
            }
         }

         recordedGesture = MouseGestureTypes.NoGesture;

         if (north || south) 
         {
            if (east || west)
            {
               // compound gesture:
               recordedGesture = interpretCompoundGesture(first, last, north,
                south, east, west);
            }
            else
            {
               // pure vertical gesture:
               if (north)
               {
                  recordedGesture = MouseGestureTypes.NorthGesture;
               }
               else
               {
                  recordedGesture = MouseGestureTypes.SouthGesture;
               }
            }
         }
         else if (east || west)
         {
            // pure horizontal gesture
            if (east)
            {
               recordedGesture = MouseGestureTypes.EastGesture;
            }
            else
            {
               recordedGesture = MouseGestureTypes.WestGesture;
            }
         }

         if (recordedGesture != MouseGestureTypes.NoGesture)
         {            
            if ((gestureTypes & recordedGesture) != 0)
            {
               MouseGestureEventArgs args = new MouseGestureEventArgs(
                  recordedGesture, first, last);
               if (this.MouseGesture != null)
               {
                  this.MouseGesture(this, args);
                  retValue = args.AcceptGesture;
               }
            }         
         }

         return retValue;
      }

      private MouseGestureTypes interpretCompoundGesture(
         Point first, Point last,
         bool north, bool south, bool east, bool west)
      {
         MouseGestureTypes retValue = MouseGestureTypes.NoGesture;

         // draw a diagonal line between start & end
         // and determine if most points are y above 
         // the line or not:
         int pointAbove = 0;
         int pointBelow = 0;
         
         foreach (Point point in gesture)
         {
            int diagY = ((point.X - first.X) * (first.Y - last.Y)) / (first.X -
             last.X) + first.Y;
            if (point.Y > diagY)
            {
               pointAbove++;
            }
            else
            {
               pointBelow++;
            }
         }

         if (north)
         {
            if (east)
            {
               if (pointAbove > pointBelow)
               {
                  retValue = MouseGestureTypes.EastThenNorthGesture;
               }
               else
               {
                  retValue = MouseGestureTypes.NorthThenEastGesture;
               }
            }
            else
            {
               if (pointAbove > pointBelow)
               {
                  retValue = MouseGestureTypes.WestThenNorthGesture;
               }
               else
               {
                  retValue = MouseGestureTypes.NorthThenWestGesture;
               }

            }
         }
         else if (south)
         {
            if (east)
            {
               if (pointAbove > pointBelow)
               {
                  retValue = MouseGestureTypes.SouthThenEastGesture;
               }
               else
               {
                  retValue = MouseGestureTypes.EastThenSouthGesture;
               }
            }
            else
            {
               if (pointAbove > pointBelow)
               {
                  retValue = MouseGestureTypes.SouthThenWestGesture;
               }
               else
               {
                  retValue = MouseGestureTypes.WestThenSouthGesture;
               }
            }
         }

         return retValue;
      }


      /// <summary>
      /// Constructs a default instance of this class.  The class
      /// checks for all <c>MouseGestureTypes</c>.
      /// </summary>       
      public MouseGestureFilter()
      {
         this.gestureTypes =  MouseGestureTypes.AllGestureTypes;
      }

      /// <summary>
      /// Constructs a new instance of this class and starts checking for
      /// the specified mouse gestures.
      /// </summary>
      /// <param name="gestureTypes"></param>
      public MouseGestureFilter(MouseGestureTypes gestureTypes)
      {
         this.gestureTypes = gestureTypes;
      }

   }
}