Drydo's Blog

Teenager of the Internet

vbCity Blogs moved to:
http://cs.vbcity.com/blogs
  Home :: Syndication  :: Login   Community Forums   :: vbCity.com   :: DevCity.NET  

I know it’s been a while. It could be you - it could be me - but hey - at least I'm taking some responsibility in this relationship.

Well, the summer's been kinda fun, changing job, not leaving said job, lots of family stuff and general messing around. During this messing around I succumbed to moving to Firefox as my main browser and whilst there are some fun elements with just the basic install; it was the various plugins the really impressed me. Now I won't run through all of the plugins I'm using - but highlight one that did really impress. Basically, its a plugin that supports mouse gestures and allows you to bind various browser commands to specific mouse gestures made when holding and releasing the right-hand mouse button and to be fair has speeded up my whole browser experience.

So, I thought, why shouldn't something like Visual Studio (2005) in this case support this? I had a look around, couldn't find anything, I had a hazy, lazy afternoon free so I tried and surprising managed to get it working. To a degree at least.

About a year ago I adjusted some code found on www.vbcity.com to act as a global mouse hook, I lifted this quite easily and threw it into an Addin. With the mechanism to detect mouse movement I added some code to capture some very basic mouse gestures and bind them to some VS2005 commands. Note: I was mainly interested in navigating be

ween tabs and closing them down. The following is the contents of the 'connect' class that initialises the mouse object when a new project is opened...

Code CopyHideScrollFull
Imports System
Imports
Microsoft.VisualStudio.CommandBars
Imports
Extensibility
Imports
EnvDTE
Imports
EnvDTE80

Public
Class Connect
Implements IDTExtensibility2
Dim _applicationObject As DTE2
Dim
_addInInstance As AddIn
Private _MControl As MouseHook
'''<summary>Implements the constructor for the Add-in object. Place your initialization code within this method.</summary>
Public
Sub New()
End
Sub
'''<summary>Implements the OnConnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being loaded.</summary>
'''
<param name='application'>Root object of the host application.</param>
'''
<param name='connectMode'>Describes how the Add-in is being loaded.</param>
'''
<param name='addInInst'>Object representing this Add-in.</param>
'''
<remarks></remarks>
Public
Sub OnConnection(ByVal application As Object, ByVal connectMode As ext_ConnectMode, ByVal addInInst As Object, ByRef custom As Array) Implements IDTExtensibility2.OnConnection
_applicationObject = CType(application, DTE2)
_addInInstance = CType(addInInst, AddIn)
_MControl = New MouseHook(Me._applicationObject.MainWindow, MouseHook.GetForegroundWindow)
AddHandler
_MControl.GestureCompleted, AddressOf UserGestureMade
End Sub
'''<summary>Implements the OnDisconnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being unloaded.</summary>
'''
<param name='disconnectMode'>Describes how the Add-in is being unloaded.</param>
'''
<param name='custom'>Array of parameters that are host application specific.</param>
'''
<remarks></remarks>
Public
Sub OnDisconnection(ByVal disconnectMode As ext_DisconnectMode, ByRef custom As Array) Implements IDTExtensibility2.OnDisconnection
If Not Me._MControl Is Nothing Then
Me._MControl.Dispose()
Me
._MControl = Nothing
End If
End Sub
'''<summary>Implements the OnAddInsUpdate method of the IDTExtensibility2 interface. Receives notification that the collection of Add-ins has changed.</summary>
'''
<param name='custom'>Array of parameters that are host application specific.</param>
'''
<remarks></remarks>
Public
Sub OnAddInsUpdate(ByRef custom As Array) Implements IDTExtensibility2.OnAddInsUpdate
End
Sub
'''<summary>Implements the OnStartupComplete method of the IDTExtensibility2 interface. Receives notification that the host application has completed loading.</summary>
'''
<param name='custom'>Array of parameters that are host application specific.</param>
'''
<remarks></remarks>
Public
Sub OnStartupComplete(ByRef custom As Array) Implements IDTExtensibility2.OnStartupComplete
End
Sub
'''<summary>Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.</summary>
'''
<param name='custom'>Array of parameters that are host application specific.</param>
'''
<remarks></remarks>
Public
Sub OnBeginShutdown(ByRef custom As Array) Implements IDTExtensibility2.OnBeginShutdown
End
Sub
Private Sub UserGestureMade(ByVal sender As Object, ByVal e As EventGestureMadeArgs)
Select Case e.GestureDirection
Case MouseHook.GestureDirectionMade.Left
Me._applicationObject.ExecuteCommand("Window.PreviousDocumentWindow")
Case MouseHook.GestureDirectionMade.Right
Me._applicationObject.ExecuteCommand("Window.NextDocumentWindow")
Case MouseHook.GestureDirectionMade.Up
Me._applicationObject.ExecuteCommand("File.Close")
Case MouseHook.GestureDirectionMade.Down
Me._applicationObject.ExecuteCommand("File.SaveSelectedItems")
End Select
End Sub

End
Class
. . .

..and this is the contents of the MouseHook class that contains the logic for the actual hook, detecting the gesture and raising an appropriate event to notify the parent when a gesture is made.

Code CopyHideScrollFull
Imports System.Runtime.InteropServices
Imports
System.Drawing
Imports
System.Windows.Forms

Public
Class MouseHook
Implements IDisposable

#Region "API Declarations"
' Use this function to install thread-specific hook.
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function SetWindowsHookEx(ByVal idHook As Integer, ByVal lpfn As HookProc, ByVal hInstance As Integer, ByVal threadId As Integer) As Integer
End Function
' Call this function to uninstall the hook.
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function UnhookWindowsHookEx(ByVal idHook As Integer) As Boolean
End Function
' Use this function to pass the hook information to next hook procedure in chain.
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public
Shared Function CallNextHookEx(ByVal idHook As Integer, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
End
Function
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public
Shared Function PostMessage(ByVal hwnd As Integer, ByVal wMsg As Integer, _
   ByVal wParam As Integer, ByVal lParam As Integer) As Integer
End
Function
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public
Shared Function GetActiveWindow() As Integer
End
Function
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public
Shared Function GetForegroundWindow() As Integer
End
Function
<DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function GetWindow(ByVal hwnd As Integer, ByVal wCmd As Integer) As Integer
End Function

#End Region

#Region "Delegates and Function pointers"
' Used to pass hook messages through
Public
Delegate Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
Private MouseHookProcedure As HookProc

#End Region

#Region "Structures"
' Keyboard data structure
<StructLayout(LayoutKind.Sequential)> Private Structure KBDLLHOOKSTRUCT
Public vkCode As Integer
Public
scanCode As Integer
Public
flags As Integer
Public
time As Integer
Public
dwExtraInfo As Integer
End Structure
' Mouse data structure
<StructLayout(LayoutKind.Sequential)> Private Structure MouseHookStruct
Public pt As POINT
Public
hwnd As Integer
Public
wHitTestCode As Integer
Public
dwExtraInfo As Integer
End Structure
<StructLayout(LayoutKind.Sequential)> Private Structure POINT
Public x As Integer
Public
y As Integer
End Structure

#End Region

#Region "Enums"
Public Enum GestureDirectionMade
Up
Down
Left
Right
End Enum
Public Enum DirectionBias
Top = 1
Left = 2
Right = 4
Bottom = 8
TopLeft = 3
TopRight = 5
BottomLeft = 10
BottomRight = 12
End Enum

#End Region

#Region "Events"
Event GestureCompleted(ByVal sender As Object, ByVal e As EventGestureMadeArgs)

#End Region

#Region "Constants"
' Top level global hook parameters
Private
Const WH_KEYBOARD_LL As Integer = 13
Private
Const WH_MOUSE_LL As Integer = 14
' Keyboard Hook parameters
Private
Const HC_ACTION As Integer = 0
Private
Const WM_KEYDOWN As Integer = &H100
Private
Const WM_KEYUP As Integer = &H101
Private
Const WM_SYSKEYDOWN As Integer = &H104
Private
Const WM_SYSKEYUP As Integer = &H105
' Mouse Hook parameters
Private
Const WM_LBUTTONDOWN As Integer = &H201
Private
Const WM_LBUTTONUP As Integer = &H202
Private
Const WM_LBUTTONDBLCLK As Integer = &H203
Private
Const WM_RBUTTONDOWN As Integer = &H204
Private
Const WM_RBUTTONUP As Integer = &H205
Private
Const WM_RBUTTONDBLCLK As Integer = &H206
Private
Const WM_MBUTTONDOWN As Integer = &H207
Private
Const WM_MBUTTONUP As Integer = &H208
Private
Const WM_MBUTTONDBLCLK As Integer = &H209
' Default buffer area for right click detections
Private
Const RIGHT_CLICK_BUFFER_DIM As Integer = 5

#End Region

#Region "Private"
' Mouse hook handle
Private
hMouseHook As Integer = 0
' Stores the initial point position
Private
_InitialRightPoint As Drawing.Point
' Links back to the calling object
Private
_MainWindow As EnvDTE.Window
' Gesture Performed
Private
_GesturePerformed As GestureDirectionMade
' Main Window Handle
Private
_hMainWindow As Integer

#End Region
Public Sub New(ByVal MainWin As EnvDTE.Window, ByVal hWindow As Integer)
' Initialise the various listeners
' Stores reference to the connect object
' Used to callback and retreive the main window for position and dimensions

Me
._MainWindow = MainWin
Me
._hMainWindow = hWindow
' Mouse hook
HookMouse()
End Sub

#Region "Mouse Hook"
Private Sub HookMouse()
' Creates a global hook for the keyboard passing a function pointer to handle the messages
MouseHookProcedure = New HookProc(AddressOf Me.MouseHookProc)
' Set the hook
Me
.hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProcedure, Marshal.GetHINSTANCE(System.Reflection.Assembly.GetCallingAssembly.GetModules()(0)).ToInt32, 0)
' Determine whether the hook was successful
If
Me.hMouseHook = 0 Then
' TODO - MouseHook: Generate appropriate error message on hook error
End If
End Sub
Private Sub UnhookMouse()
' Undoes the global mouse hook
If
Me.hMouseHook <> 0 Then
' Unhook
Dim
ret As Boolean = UnhookWindowsHookEx(hMouseHook)
' Determine whether the unhook was successful
If
ret = False Then
' TODO - MouseHook: Generate appropriate error message on Unhook error
End If
End If
End Sub
Public Function MouseHookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
' Convert the data from the message
Dim
MyMouseHookStruct As MouseHookStruct = CType(Marshal.PtrToStructure(lParam, GetType(MouseHookStruct)), MouseHookStruct)
' Can the message be handled
If
(nCode >= 0) Then
' Are we listneing to this message
' Mouse click
Select
Case wParam.ToInt32
Case WM_RBUTTONDOWN
' Set the position of the right click being made
_InitialRightPoint = New Drawing.Point(MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y)
Case WM_RBUTTONUP
' Raise an appropriate event for Mouse Click
Debug.WriteLine("Right Up heard")
Dim ReleasePoint As New Drawing.Point(MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y)
' Was the release outside of the buffer zone?
If
OutsideOfBufferArea(ReleasePoint) AndAlso IsOnWindow() Then
' Determine the action to perform
DetermineActionMade(ReleasePoint)
' Prevent any further actions on this click
' Attempt to generate a new right up to allow new messages to be generated
PostMessage(MouseHook.GetForegroundWindow, WM_RBUTTONUP, wParam.ToInt32, &H7FFF7FFF)
PostMessage(MyMouseHookStruct.hwnd, WM_RBUTTONUP, wParam.ToInt32, &H7FFF7FFF)
' Consume this message
Return
1
End If
End Select
End If
' Pass the message along
Return
CallNextHookEx(Me.hMouseHook, nCode, wParam, lParam)
End Function

#End Region

#Region "Detection Logic"
Private Function IsOnWindow() As Boolean
' Returns whether the initial click was made upon the main window
Dim WinArea As New Rectangle( _
Me._MainWindow.Left, _
Me
._MainWindow.Top, _
Me
._MainWindow.Height, _
Me
._MainWindow.Width)
Return WinArea.Contains(Me._InitialRightPoint)
End Function
Private Sub DetermineActionMade(ByVal RightClickRelease As Drawing.Point)
' This routine is responsible for determining the action made by checking the position of the start and
' release points and generating the appropriate event.
Dim Direction As DirectionBias
' Are we going Top or Bottom?
If
Me._InitialRightPoint.Y < RightClickRelease.Y Then
' Bottom
Direction = DirectionBias.Bottom
Else
' Top
Direction = DirectionBias.Top
End If
' Are we going left or right?
If
Me._InitialRightPoint.X > RightClickRelease.X Then
' Left
Direction = Direction Or DirectionBias.Left
Else
' Right
Direction = Direction Or DirectionBias.Right
End If
Dim DirectionGesture As GestureDirectionMade
Dim
HorizontalDiff As Integer = Math.Abs(Me._InitialRightPoint.X - RightClickRelease.X)
Dim
VerticalDiff As Integer = Math.Abs(Me._InitialRightPoint.Y - RightClickRelease.Y)
' For each case - determine the actual direction
Select
Case Direction
Case DirectionBias.TopLeft
If HorizontalDiff > VerticalDiff Then
DirectionGesture = GestureDirectionMade.Left
Else
DirectionGesture = GestureDirectionMade.Up
End If
Case DirectionBias.TopRight
If HorizontalDiff > VerticalDiff Then
DirectionGesture = GestureDirectionMade.Right
Else
DirectionGesture = GestureDirectionMade.Up
End If
Case DirectionBias.BottomLeft
If HorizontalDiff > VerticalDiff Then
DirectionGesture = GestureDirectionMade.Left
Else
DirectionGesture = GestureDirectionMade.Down
End If
Case DirectionBias.BottomRight
If HorizontalDiff > VerticalDiff Then
DirectionGesture = GestureDirectionMade.Right
Else
DirectionGesture = GestureDirectionMade.Down
End If
End Select
' When completed - record the gesture performed
_GesturePerformed = DirectionGesture
' Bit of a fugde - but we use a timer to fire the event.  For some reason, without this
' code will ensure that the system still consumes the Up event :-S
'Threading.Thread.Sleep(10)
'RaiseEvent GestureCompleted(Me, New EventGestureMadeArgs(Me._GesturePerformed))
Dim t As New Timers.Timer
With
t
.Enabled = False
.AutoReset = False
.Interval = 10
AddHandler
.Elapsed, AddressOf FireGestureThread
.Enabled = True
End With
End Sub
Private Sub FireGestureThread(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs)
RaiseEvent GestureCompleted(Me, New EventGestureMadeArgs(Me._GesturePerformed))
End Sub
Private Function OutsideOfBufferArea(ByVal RightClickRelease As Drawing.Point) As Boolean
' This routine is responsible for determining whether point passed through is outside
' of the buffer area...

Dim
BufferArea As New Rectangle( _
Me._InitialRightPoint.X - RIGHT_CLICK_BUFFER_DIM, _
Me._InitialRightPoint.Y - RIGHT_CLICK_BUFFER_DIM, _
RIGHT_CLICK_BUFFER_DIM * 2, _
RIGHT_CLICK_BUFFER_DIM * 2)
Return Not BufferArea.Contains(RightClickRelease)
End Function

#End Region
Public Sub Dispose() Implements System.IDisposable.Dispose
' Dispose of the appropriate objects
' Unhook the mouse
UnhookMouse()
End Sub

End
Class

Public
Class EventGestureMadeArgs
Inherits System.EventArgs
Private _GestureDir As MouseHook.GestureDirectionMade
Public Sub New(ByVal GestureDirection As MouseHook.GestureDirectionMade)
_GestureDir = GestureDirection
End Sub
Public ReadOnly Property GestureDirection() As MouseHook.GestureDirectionMade
Get
Return _GestureDir
End Get
End Property

End
Class
. . .

The gestures work within the Studio environment and I've provided the source code so you can simply create your own Addin and drop the code directly into it. So what's the problem with it?

Well, when a gesture is performed we consume the Right-Up event within our code. Unfortunately, this then prevents *any* left mouse clicks to any other windows. The reason is that Windows is expected a MouseUp event to occur after a MouseDown event and if we consume this the OS gets a bit stroppy. To activate another window, you must perform a right-click upon it, or don't consume the MouseUp event and cause any context menu to appear (which we are trying to prevent as it shouldn't appear in the context of performing a legitmate mouse gesture).

Now after a bit of quick research the approach to take is to simply post a Mouse Right Up event to the calling window - but outside of its screen bounds to keep the circle of life flowing. Unfortunately, this is the bit I've been unable to get working and reason I posting up the code as WIP for either someone to use (but knowing the above caveats) or to suss out themselves (in which case, drop me a line and tell me how you got around it :-)

There's 101 things that a proper Addin would include and would have been include if not for the stumbling block above, e.g. mapable commands through user XML file, more complicated gesture detection, providing visual feedback of gesture made, etc. But it is something and was a bit of fun at the time…

Have Fun - M

posted on Monday, September 11, 2006 2:19 PM