using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;
using System.ComponentModel;
namespace DcamMouseGesture
{
#region delegates & event args
public class MouseGestureEventArgs : EventArgs
{
public string Key { get; set; }
public string Command { get; set; }
public Rectangle Bounds { get; set; }
public Control Control { get; set; }
public MouseGestureEventArgs( string strKey, string strCommand, Rectangle rBounds, Control ctrl )
{
Key = strKey;
Command = strCommand;
Bounds = rBounds;
Control = ctrl;
}
}
public delegate void MouseGestureEventHandler( object sender, MouseGestureEventArgs e );
#endregion // delegates & event args
public class MouseGesture : IDisposable
{
#region enums
internal enum EKeyOperation
{
MirrorX,
MirrorY,
MirrorXY,
RotateCW,
RotateCCW
}
#endregion // enums
#region constants & variables
// for Base64 mapping table see http://msdn2.microsoft.com/en-us/library/cc422512.aspx
private const string __strBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private const int __iSize = 6;
internal const int __iGestureThumbSize = 32;
// capturing
private bool _bDisposed = false; // for the implementation of the IDisposable interface
private MouseGestureData _data = MouseGestureData.Instance; // use a variable to have a shorter name
private Graphics _g = null;
private Pen _penLine = null;
private Control _ctrlParent = null; // the parent control
private CaptureForm _formCapture = new CaptureForm(); // the form to draw the capture points onto
private List<Point> _listMousePos = new List<Point>(); // the captured mouse positions, relative to 0/0 of the parent
// control or of the client rectangle of the parent Form
private List<Point> _listPath = new List<Point>(); // the path of the mouse gesture in x/y coordinates of a square of __iSize * __iSize fields
// used to draw the gesture while entering it
private bool _bCapturing = false; // true if capturing is in progress
private int _iDrawOffsetX = 0; // because of client and screen coordinates difference
private int _iDrawOffsetY = 0;
private int _iMouseOffsetX = 0;
private int _iMouseOffsetY = 0;
private Point _pntNew;
private Point _pntLast; // last point drawn, ussed for drawing the lines
private Timer _timerDelay = new Timer(); // timer to delay hiding the capture window
// when last gesture found...
public Rectangle LastBounds { get; private set; } // bounding rectangle; 0/0/0/0 if not found
public string LastKey { get; private set; } // key; empty if not found
public string LastCommand { get; private set; } // appropriate command; empty if not found
public Control LastControl { get; private set; } // the control the gesture started in; null if not found
// events
public event MouseGestureEventHandler MouseGestureEntered;
#endregion // constants & variables
#region construction, initialization & destruction
public MouseGesture( Control ctrlParent, List<Type> listAllowedControls )
{
if( ctrlParent == null ) throw new ArgumentNullException();
_ctrlParent = ctrlParent;
// init automatic properties
LastBounds = new Rectangle();
LastKey = string.Empty;
LastCommand = string.Empty;
InitData();
// setup the timer
_timerDelay.Interval = _data.HideDelay;
_timerDelay.Tick += new EventHandler( OnTimerTick );
// setup the capturing window
_formCapture.Hide();
_formCapture.Name = "CaptureForm";
_formCapture.Text = "CaptureForm";
_formCapture.ClientSize = new System.Drawing.Size( 500, 500 );
_formCapture.ShowInTaskbar = false;
_formCapture.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
_formCapture.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
_formCapture.BackColor = Color.WhiteSmoke;
_formCapture.Opacity = _data.Opacity;
// make sure null uses default list
RegisterMouseEvents( _ctrlParent, listAllowedControls == null ? GetDefaultAllowedControls() : listAllowedControls );
}
~MouseGesture()
{
Dispose( false );
DisposeGlobalResources();
}
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( this );
}
private void Dispose( bool disposing )
{
if( !_bDisposed )
{
if( disposing )
{
UnregisterMouseEvents( _ctrlParent );
_timerDelay.Tick -= new EventHandler( OnTimerTick );
}
_bDisposed = true;
}
}
#endregion // construction, initialization & destruction
#region instance methods
internal static Bitmap CreateThumbBitmap( string strKey )
{
if( string.IsNullOrEmpty( strKey ) ) throw new ArgumentNullException( Properties.Resources.MouseGesture_ErrorEmptyKey );
Bitmap bmpRet = new Bitmap( __iGestureThumbSize, __iGestureThumbSize );
Graphics g = Graphics.FromImage( bmpRet );
// draw bitmap
try
{
int iStretch = 3;
int iOffset = ( __iGestureThumbSize - iStretch * __iSize ) / 2;
// draw second to second last points
for( int i = 1; i < strKey.Length - 1; i++ )
{
int iX = __strBase64.IndexOf( strKey[ i ] ) % __iSize;
int iY = __strBase64.IndexOf( strKey[ i ] ) / __iSize;
// only draw the point if it is a valid index
if( iX == -1 || iY == -1 ) throw new Exception();
g.FillRectangle( Brushes.LightPink, iOffset + iX * iStretch, iOffset + iY * iStretch, iStretch, iStretch );
}
// draw first and last point to make sure they are visible
int iX0 = __strBase64.IndexOf( strKey[ 0 ] ) % __iSize;
int iY0 = __strBase64.IndexOf( strKey[ 0 ] ) / __iSize;
int iX1 = __strBase64.IndexOf( strKey[ strKey.Length - 1 ] ) % __iSize;
int iY1 = __strBase64.IndexOf( strKey[ strKey.Length - 1 ] ) / __iSize;
// both points are at the same position
if( iX0 == iX1 && iY0 == iY1 )
{
g.FillRectangle( Brushes.DarkViolet, iOffset + iX0 * iStretch, iOffset + iY0 * iStretch, iStretch, iStretch );
}
else
{
g.FillRectangle( Brushes.Blue, iOffset + iX0 * iStretch, iOffset + iY0 * iStretch, iStretch, iStretch );
g.FillRectangle( Brushes.Red, iOffset + iX1 * iStretch, iOffset + iY1 * iStretch, iStretch, iStretch );
}
}
catch
{
// draw a red X on white background with blue text "Error"
g.Clear( Color.White );
g.DrawLine( Pens.Red, 0, 0, __iGestureThumbSize - 1, __iGestureThumbSize - 1 );
g.DrawLine( Pens.Red, 0, __iGestureThumbSize - 1, __iGestureThumbSize - 1, 0 );
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
g.DrawString( "Error", new Font( "Arial", 8f ), Brushes.Blue, __iGestureThumbSize / 2, __iGestureThumbSize / 2, sf );
}
g.Dispose();
return bmpRet;
}
internal static string MirrorRotateKey( string strKey, EKeyOperation keyOp )
{
StringBuilder sbKey = new StringBuilder( strKey.Length );
for( int i = 0; i < strKey.Length; i++ )
{
int iIndex = __strBase64.IndexOf( strKey[ i ] );
int iX = iIndex % __iSize;
int iY = iIndex / __iSize;
switch( keyOp )
{
case EKeyOperation.MirrorX:
sbKey.Append( __strBase64[ ( __iSize - 1 - iX ) + ( iY * __iSize ) ] );
break;
case EKeyOperation.MirrorY:
sbKey.Append( __strBase64[ iX + ( ( __iSize - 1 - iY ) * __iSize ) ] );
break;
case EKeyOperation.MirrorXY:
sbKey.Append( __strBase64[ ( __iSize - 1 - iX ) + ( ( __iSize - 1 - iY ) * __iSize ) ] );
break;
case EKeyOperation.RotateCW:
sbKey.Append( __strBase64[ ( __iSize - 1 - iY ) + ( iX * __iSize ) ] );
break;
case EKeyOperation.RotateCCW:
sbKey.Append( __strBase64[ iY + ( ( __iSize - 1 - iX ) * __iSize ) ] );
break;
default:
throw new NotImplementedException();
}
}
return sbKey.ToString();
}
public static List<Type> GetDefaultAllowedControls()
{
return new List<Type>() { typeof( Label ), typeof( GroupBox ), typeof( PictureBox ),
typeof( ProgressBar ), typeof( ScrollableControl ), typeof( TabControl ) };
}
private void RegisterMouseEvents( Control ctrlParten, List<Type> listAllowedControls )
{
// Note:
// We are not registering for the mouse events on all type of controls,
// only some sort of "container controls". Meaning that these are controls that "show"
// some sort of the parent control's background and not having selectable elements and
// not being data entering controls.
// Also assuming that only such controls can have other "container controls", if any;
// (e.g. a GroupBox that is not allowed can't contain a ListBox that is allowed)
bool bOk = false;
foreach( Type t in listAllowedControls )
{
if( t.IsInstanceOfType( ctrlParten ) )
{
bOk = true;
break;
}
}
if( !bOk ) return;
// register passed control
ctrlParten.MouseDown += new MouseEventHandler( OnMouseDown );
ctrlParten.MouseUp += new MouseEventHandler( OnMouseUp );
ctrlParten.MouseMove += new MouseEventHandler( OnMouseMove );
// recursive call of all containing controls
foreach( Control ctrl in ctrlParten.Controls )
{
RegisterMouseEvents( ctrl, listAllowedControls );
}
}
private void UnregisterMouseEvents( Control ctrlParten )
{
// unregister for all control, so we don't need a list of the allowed controls
ctrlParten.MouseDown -= new MouseEventHandler( OnMouseDown );
ctrlParten.MouseUp -= new MouseEventHandler( OnMouseUp );
ctrlParten.MouseMove -= new MouseEventHandler( OnMouseMove );
// recursive call of all containing controls
foreach( Control ctrl in ctrlParten.Controls )
{
UnregisterMouseEvents( ctrl );
}
}
private void DisposeGlobalResources()
{
if( _g != null )
{
_g.Dispose();
_g = null;
}
if( _penLine != null )
{
_penLine.Dispose();
_penLine = null;
}
_formCapture.DisposeBackgroundBitmap();
}
private void InitData()
{
_listMousePos.Clear();
_listPath.Clear();
_iDrawOffsetX = 0;
_iDrawOffsetY = 0;
_pntLast = new Point( -1000000, -1000000 ); // make this invalid
LastBounds = new Rectangle();
LastKey = string.Empty;
LastCommand = string.Empty;
LastControl = null;
}
private void AddMousePoint( Point pnt )
{
_pntNew = new Point( _iMouseOffsetX + pnt.X, _iMouseOffsetY + pnt.Y );
_listMousePos.Add( _pntNew );
}
private void DrawMousePoint()
{
switch( _data.GestureAppearance )
{
case EGestureAppearance.Points:
_g.DrawLine( _penLine, _iDrawOffsetX + _pntNew.X, _iDrawOffsetY + _pntNew.Y,
_iDrawOffsetX + _pntNew.X + 1, _iDrawOffsetY + _pntNew.Y + 1 );
break;
case EGestureAppearance.Lines:
if( _pntLast.X > -1000000 )
{
_g.DrawLine( _penLine, _iDrawOffsetX + _pntLast.X, _iDrawOffsetY + _pntLast.Y,
_iDrawOffsetX + _pntNew.X, _iDrawOffsetY + _pntNew.Y );
}
_pntLast = _pntNew;
break;
}
}
private void DrawBoundsGridPath()
{
if( _data.ShowBounds )
{
Pen penBounds = new Pen( _data.BoundsColor );
Rectangle rect = new Rectangle( _iDrawOffsetX + LastBounds.X, _iDrawOffsetY + LastBounds.Y, LastBounds.Width, LastBounds.Height );
_g.DrawRectangle( penBounds, rect );
penBounds.Dispose();
if( _data.ShowGrid )
{
Pen penGrid = new Pen( _data.GridColor );
for( int i = 1; i < __iSize; i++ )
{
_g.DrawLine( penGrid, rect.X + rect.Width * i / __iSize, rect.Y, rect.X + rect.Width * i / __iSize, rect.Bottom );
_g.DrawLine( penGrid, rect.X, rect.Y + rect.Height * i / __iSize, rect.Right, rect.Y + rect.Height * i / __iSize );
}
penGrid.Dispose();
}
}
if( _data.ShowPath )
{
Brush brush = new SolidBrush( _data.PathColor );
Rectangle rect = new Rectangle( 0, 0, LastBounds.Width / __iSize, LastBounds.Height / __iSize );
for( int i = 0; i < _listPath.Count; i++ )
{
rect.X = _iDrawOffsetX + LastBounds.X + _listPath[ i ].X * LastBounds.Width / __iSize;
rect.Y = _iDrawOffsetY + LastBounds.Y + _listPath[ i ].Y * LastBounds.Height / __iSize;
_g.FillRectangle( brush, rect );
}
brush.Dispose();
}
}
private void CreateKeyAndPath()
{
// Note:
// - The path of the mouse gesture is in x/y coordinates of a square of __iSize * __iSize fields
// --> so top/left square = 0/0, bottom/right square = 7/7.
// - The key is a string of Base64 maped charactes, where each character defines a field
// number in the square of __iSize * __iSize fields. The fields are numbered from 0 to 63 from
// left to right and from top to buttom.
// --> so top/left square = 0, bottom/right square = 63.
StringBuilder sbKey = new StringBuilder( 100 );
Point pntLast = new Point( -1, -1 );
Point pntPath;
// using only X axis (Y axis is always 0 and won't be added to the key)
if( LastBounds.Height / __iSize < _data.UnidimensionalLimit )
{
for( int i = 0; i < _listMousePos.Count; i++ )
{
pntPath = new Point( ( _listMousePos[ i ].X - LastBounds.X ) * __iSize / LastBounds.Width, 0 );
if( pntPath.X != pntLast.X )
{
_listPath.Add( pntPath );
pntLast = pntPath;
sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
}
}
}
// ...using only Y axis (X axis is always 0 and won't be added to the key)
else if( LastBounds.Width / __iSize < _data.UnidimensionalLimit )
{
for( int i = 0; i < _listMousePos.Count; i++ )
{
pntPath = new Point( 0, ( _listMousePos[ i ].Y - LastBounds.Y ) * __iSize / LastBounds.Height );
if( pntPath.Y != pntLast.Y )
{
_listPath.Add( pntPath );
pntLast = pntPath;
sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
}
}
}
// ...using x and Y axis = two-dimensional
else
{
for( int i = 0; i < _listMousePos.Count; i++ )
{
pntPath = new Point( ( _listMousePos[ i ].X - LastBounds.X ) * __iSize / LastBounds.Width,
( _listMousePos[ i ].Y - LastBounds.Y ) * __iSize / LastBounds.Height );
if( pntPath.X != pntLast.X || pntPath.Y != pntLast.Y )
{
_listPath.Add( pntPath );
pntLast = pntPath;
sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
}
}
}
LastKey = sbKey.ToString();
}
private void AnalysePoints()
{
// search bounding rectangle
int iMinX, iMaxX, iMinY, iMaxY;
iMinX = iMaxX = _listMousePos[ 0 ].X;
iMinY = iMaxY = _listMousePos[ 0 ].Y;
for( int i = 1; i < _listMousePos.Count; i++ )
{
Point pnt = _listMousePos[ i ];
if( pnt.X < iMinX ) iMinX = pnt.X;
if( pnt.Y < iMinY ) iMinY = pnt.Y;
if( pnt.X > iMaxX ) iMaxX = pnt.X;
if( pnt.Y > iMaxY ) iMaxY = pnt.Y;
}
LastBounds = new Rectangle( iMinX, iMinY, iMaxX - iMinX + 1, iMaxY - iMinY + 1 );
// check for minimum movement
if( LastBounds.Width >= _data.MinimumMovement || LastBounds.Height >= _data.MinimumMovement )
{
CreateKeyAndPath();
LastCommand = _data.Commands.GetCommand( LastKey );
}
else
{
InitData();
}
}
#endregion // instance methods
#region event handlers
private void OnMouseDown( object sender, MouseEventArgs e )
{
// only enter capturing if...
if( !_bCapturing && // ...not capturing in progress
e.Button == _data.MouseButtonTrigger && // ...the button pressed last to trigger
Control.MouseButtons == _data.MouseButtonMask && // ...this/these button/s pressed together
Control.ModifierKeys == _data.ModifierKeyMask // ...this/these modifier key/s (shift/ctrl/alt)
)
{
InitData();
LastControl = (Control)sender;
_bCapturing = true;
// set position and size of the capture window and calc the offsets
Point pntParent = _ctrlParent.PointToScreen( new Point() );
Point pntSender = ( (Control)sender ).PointToScreen( new Point() );
_iMouseOffsetX = pntSender.X - pntParent.X;
_iMouseOffsetY = pntSender.Y - pntParent.Y;
switch( _data.WindowAppearance )
{
case ECaptureWindowAppearance.FullScreenOpaque:
_formCapture.Location = Screen.PrimaryScreen.Bounds.Location;
_formCapture.Size = Screen.PrimaryScreen.Bounds.Size;
_iDrawOffsetX = pntParent.X;
_iDrawOffsetY = pntParent.Y;
break;
case ECaptureWindowAppearance.ParentOpaque:
case ECaptureWindowAppearance.ParentClear:
_formCapture.Location = pntParent;
// the Form's Size includes title and border, but we only want the client area
Form formTmp = _ctrlParent as Form;
_formCapture.Size = ( formTmp != null ) ? formTmp.ClientSize : _ctrlParent.Size;
if( _data.WindowAppearance == ECaptureWindowAppearance.ParentClear )
{
_formCapture.CaptureBackgroundBitmap();
}
break;
}
// add first point
AddMousePoint( e.Location );
// if gesture window desired
if( _data.WindowAppearance != ECaptureWindowAppearance.None )
{
// make it visible
_formCapture.Opacity = _data.Opacity;
_formCapture.BackColor = _data.BackColor;
_formCapture.Show();
// create global resources to draw
_g = _formCapture.CreateGraphics();
_penLine = new Pen( _data.LineColor, _data.LineWidth );
// finaly draw the first point
DrawMousePoint();
}
}
}
private void OnMouseMove( object sender, MouseEventArgs e )
{
if( _bCapturing )
{
AddMousePoint( e.Location );
if( _data.WindowAppearance != ECaptureWindowAppearance.None ) DrawMousePoint();
}
}
private void OnMouseUp( object sender, MouseEventArgs e )
{
if( _bCapturing )
{
// from now on, capturing the mouse positions is finished, no matter
// if a gesture will be found or not
_bCapturing = false;
// the trigger button must be released to successfully end capturing
if( e.Button == _data.MouseButtonTrigger )
{
AddMousePoint( e.Location );
// DrawPoint not needed, because MouseUp event raises after last MouseMove event
AnalysePoints();
// draw result
if( _data.WindowAppearance != ECaptureWindowAppearance.None )
{
DrawBoundsGridPath();
_timerDelay.Interval = _data.HideDelay;
_timerDelay.Start();
}
else
{
DisposeGlobalResources();
_formCapture.Hide();
}
// rais event
if( MouseGestureEntered != null )
{
MouseGestureEntered( this, new MouseGestureEventArgs( LastKey, LastCommand, LastBounds, LastControl ) );
}
}
// otherwise just clear the data and hide window
else
{
InitData();
DisposeGlobalResources();
_formCapture.Hide();
}
}
}
private void OnTimerTick( object sender, EventArgs e )
{
_timerDelay.Stop();
if( _g != null )
{
// clear the bounding rectangle, otherwise it will
// be drawn when showing the window again
Brush brush = new SolidBrush( _formCapture.BackColor );
_g.FillRectangle( brush, LastBounds );
brush.Dispose();
DisposeGlobalResources();
}
_formCapture.Hide();
}
#endregion // event handlers
}
}