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...
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.
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