; TODO,
; remove debug hotkey - and ea() func
; reconsider choise of default cursor when A_Cursor is Unknown.
; Manage click up when more than mouse is moving and click is down....
class Cursor extends _UCR.Classes.Plugin
{
type:="Cursor"
description:="Mulitple cursor controller"
static AllCursors:=[] ; Tracks all instances of this plugin
static MouseIdCounter:=[] ; For tracking multiple active cursor from the same mouse id.
static doNothingFunc:=ObjBindMethod(Cursor, "doNothing") ; For disabling mouse buttons.
init()
{
this.LoadCursor() ; Loads cursor handles.
; Gui
; Some control(s) are placed at rowX, colY, defined here:
row0:=" y5 ", row0_:=" y2 "
row1:=" y35 ", row1_:=" y32 "
row2:=" y65 ", row2_:=" y62 "
row3:=" y95 ", row3_:=" y92 "
row4:=" y125 ", row4_:=" y123 "
row5:=" y155 ", row5_:=" y153 "
row6:=" y185 ", row6_:=" y183 "
row7:=" y215 ", row7_:=" y213 "
; Columns for gui placement
col1:=" x25 ", col2:=" x145 ", col3:=" x260 ", col4:=" x375 ", col5:=" x490 "
Gui, Add, Tab2, w650 h65, % "Main|Cycle|Coordinates|Help"
Gui, Tab, 1
; Mouse selection
Gui, Add, Button, % " w100 h25 hwndselectMouseButton", % "Select mouse"
this.selectMouseButton:=selectMouseButton
gFunc := this.SelectMouseInit.Bind(this)
GuiControl, +g, % this.selectMouseButton, % gFunc
this.AddControl("Edit", "MouseIdEdit", this.MouseIdSet.Bind(this),"x+10 yp+3 w75 center")
this.AddControl("InputButton", "toggle", 0, this.toggle.Bind(this),"x+10 h25")
; Toggle light
this.AddControl("ButtonPreview", "bp1", 0,0, "x+10")
; Sensitivity
Gui,Add,Text,x+10 yp+3,Sensitivity:
this.AddControl("Edit", "sensEdit", this.sensChanged.Bind(this),"x+10 yp-3 w35 center",1)
; Hide/show option
this.AddControl("Checkbox", "hideWhenInactiveCB", this.hideWhenInactiveCBChanged.Bind(this),"x+10 yp+3 checked", "Hide cursor when inactive")
Gui, Tab, 2
Gui,Add,Text,,Cycle cursors:
this.AddControl("InputButton", "cycleCursor", 0, this.cycleCursor.Bind(this),"x+10 h25")
this.AddControl("Checkbox", "cycleCursorCB", 0,"x+10 yp+3 checked", "Cycle by id")
; Coordinates tab
Gui, Tab, 3
Gui,Add,Text, x+5,Confine to: (x1,y1)
this.AddControl("Edit", "confineX1", this.confineEditChanged.Bind(this,1),"x+5 yp-3 w35 number center",0)
this.AddControl("Edit", "confineY1", this.confineEditChanged.Bind(this,2),"x+5 w35 number center",0)
Gui,Add,Text, x+5 yp+3,(x2,y2)
this.AddControl("Edit", "confineX2", this.confineEditChanged.Bind(this,3),"x+5 yp-3 w35 number center",A_ScreenWidth)
this.AddControl("Edit", "confineY2", this.confineEditChanged.Bind(this,4),"x+5 w35 number center",A_ScreenHeight)
Gui,Add,Text, x+5 yp+3,Start at: (x,y)
this.AddControl("Edit", "startX1", this.startEditChanged.Bind(this,1),"x+5 yp-3 w35 number center",A_ScreenWidth//2)
this.AddControl("Edit", "startY1", this.startEditChanged.Bind(this,2),"x+5 w35 number center",A_ScreenHeight//2)
Gui, Add, Button, % "x+5 hwndresetButton", % "Reset"
this.resetButton:=resetButton
gFunc := this.resetCoords.Bind(this)
GuiControl, +g, % this.resetButton, % gFunc
; Help tab
Gui, Tab, 4
Gui, Font, s8 w400, Courier New
Gui, add, Link, % row1 col1, % "Author: Helgef (2017-03-03). "
. "Instructions are available at: AutoHotkey.com forum.`n"
; Misc.
this.createCursor() ; Make the cursor's gui.
Cursor.AllCursors.Push(this) ; The plugin is self aware.
this.hasBeenToggled:=0
this.hideWhenInactive && !this.toggleState ? this.hideCursor() : this.showCursor()
; DEBUG - REMOVE
; ff:=ObjBindMethod(Cursor,"ea")
; try
; Hotkey, Esc, % ff
}
; Clean up for UCR.
onClose(){
GuiControl, -g, % this.selectMouseButton
GuiControl, -g, % this.resetButton
for k, cur in Cursor.AllCursors ; Find position in AllCursors list.
if (cur=this)
break
Cursor.AllCursors.RemoveAt(k) ; Remove from AllCursors list
Cursor.MouseIdCounter.HasKey(this.SelectedMouse) ? --Cursor.MouseIdCounter[this.SelectedMouse] : "" ; Decerment mouse id counter
if !Cursor.AllCursors.length()
Cursor.md.Delete(),Cursor.md:="",Cursor.toggleHotkeys("Off") ; Remove the mouse delta if no more cursor plugins exists.
this.destroyCursor() ; Destroy the gui cursor.
base.onClose() ;?
}
; Gui functions
; Coords tab
confineEditChanged(num,val){ ; Confinement rectangle.
if (num=1)
this.minX:=val
else if (num=2)
this.minY:=val
else if (num=3)
this.maxX:=val
else if (num=4)
this.maxY:=val
return
}
startEditChanged(num,val){ ; start position
if (num=1)
this.oX:=val
else if (num=2)
this.oY:=val
return
}
resetCoords(){ ; Reset coords button
this.GuiControls.confineX1.Set(0)
this.GuiControls.confineY1.Set(0)
this.GuiControls.confineX2.Set(A_ScreenWidth)
this.GuiControls.confineY2.Set(A_ScreenHeight)
this.GuiControls.startX1.Set(A_ScreenWidth//2)
this.GuiControls.startY1.Set(A_ScreenHeight//2)
}
sensChanged(val){
if val is number
return this.sens:=val ; Set sensitivity
return
}
hideWhenInactiveCBChanged(state){
state && !this.toggleState ? this.hideCursor() : this.showCursor() ; Show/hide cursor to match setting.
return this.hideWhenInactive:=state ; Indicates wether cursor is hidden/shown when inactive
}
createCursor(){
if this.hWin
return ;?
this.x:=this.oX
this.y:=this.oY
Gui, new, +hwndhWin -caption +toolwindow +alwaysontop +E0x20
this.hWin:=hWin
Gui, % hWin ": Margin",0,0
Gui, % hWin ": Color", abcdef
Gui, % hWin ": Add", Picture, % "x0 y0 hwndhPic", % "hIcon:*" this.hIcons["Arrow"]
this.hPic:=hPic
this.setCursor()
this.hs:=Cursor.GetCursorHotspot()
GuiControl,, % this.hPic, % "hIcon:*" this.hIcons[this.IDC] ; Consider to remove this.
;Gui, % hWin ": Show", % "Hide NA x" this.x " y" this.y
this.hidden:=1
WinSet, TransColor, abcdef 255, % "ahk_id " hWin
return
}
; Handle mouse selection
SelectMouseInit(){
; Starts the mouse selection procedure.
; How to:
; - Press the button
; - Move the mouse within 5 seconds.
; - Done!
p:=this.SelectedMouse ; Previous mouse
selectMouseMD:= new Cursor.MouseDelta(ObjBindMethod(this,"SelectMouse"))
selectMouseMD.Start()
timeout:=A_TickCount ; For timeout
while (this.SelectedMouse=p && A_TickCount-timeout<5000) ; 5 second timeout.
Sleep,-1
if (this.SelectedMouse!=p){
Cursor.MouseIdCounter.HasKey(p) ? --Cursor.MouseIdCounter[p] : "" ; A new mouse chosen
Cursor.MouseIdCounter.HasKey(this.SelectedMouse) ? ++Cursor.MouseIdCounter[this.SelectedMouse] : (Cursor.MouseIdCounter[this.SelectedMouse]:=1)
}
selectMouseMD.Delete() ; Delete the old mouse delta
return
}
MouseIdSet(id){
return this.SelectedMouse:=id
}
SelectMouse(MouseID,dx,dy) {
; Callback function for temporary mouse delta instance.
if (this.selectedMouse=MouseID)
return
this.GuiControls.MouseIdEdit.Set(MouseID)
return
}
; destroy/hide/show/moveCursor() - these functions apply to the "gui cursor" not the real windows cursor.
destroyCursor(){
if !this.hWin
return
Gui, % this.hWin ": Destroy"
return this.hWin:=""
}
hideCursor(){
if !this.hWin
return
if this.hidden
return
Gui, % this.hWin ": Show", % "NA hide"
this.hidden:=1
return
}
showCursor(move:=0){
; Consider toggle aot to avoid getting under other aot win.
if !this.hWin
return
if (this.hidden && move)
return this.showCursor()
else if (!this.hidden && !move)
return
this.hidden:=0
if (this.IDC!=this.pIDC) ; Only update if changed
GuiControl,, % this.hPic, % "hIcon:*" this.hIcons[this.IDC]
this.pIDC:=this.IDC
if !move
Gui, % this.hWin ": Show", % "NA x" this.x-this.hs[1] " y" this.y-this.hs[2] ; Adjustment for hotspot.
else
WinMove, % "ahk_id " this.hWin,, this.x-this.hs[1], this.y-this.hs[2]
return
}
moveCursor(){
return this.showCursor(true)
}
ea(){ ; remove
ExitApp
}
; For suppressing native mouse button functionality
doNothing(){
return
}
; Main togle
toggle(state) {
if !state
return
if !this.hasBeenToggled { ; Add to MouseIdCounter on first time toggle on.
Cursor.MouseIdCounter.HasKey(this.SelectedMouse) ? ++Cursor.MouseIdCounter[this.SelectedMouse] : (this.SelectedMouse ? (Cursor.MouseIdCounter[this.SelectedMouse]:=1) : "")
this.hasBeenToggled:=true
}
this.toggleState:=!this.toggleState
if (!Cursor.md && this.toggleState) {
Cursor.md:=new Cursor.MouseDelta(ObjBindMethod(Cursor,"MoveCursors")) ; Create a new mousedelta for the Cursor class, if non exists.
}
Cursor.globalToggle(this.toggleState)
if !this.toggleState {
if (this.hideWhenInactive) ; Hide/show according to settings
this.hideCursor()
else
this.showCursor()
} else {
this.showCursor()
}
this.GuiControls.bp1.SetState(this.toggleState) ; Green light when on (in UCR gui)
return
}
globalToggle(state){
; Sets the correct state of the MouseDelta: Cursor.md, depending on all toggleStates of all cursors in Cursor.AllCursors
mdState:=Cursor.md.State
if (state=mdState) ; State is matched, do nothing
return
else if (state && !mdState) { ; State is on, and md is off, turn on md.
Cursor.toggleHotkeys("On")
Cursor.md.Start()
return
}else if (!state && mdState) { ; State is off, but md is on, check if any other cursor is on, then do nothing, if no other is on, restore mouse functionality and turn off md.
for k, cur in Cursor.AllCursors
if cur.toggleState
return
Cursor.toggleHotkeys("Off")
Cursor.md.Stop()
}
return
}
toggleHotkeys(state){
; Normal mouse button functionality is suppressed while a cursor is active.
hkfn:=Cursor.doNothingFunc
Hotkey, *LButton, % hkfn, % state
Hotkey, *RButton, % hkfn, % state
Hotkey, *MButton, % hkfn, % state
Hotkey, *WheelUp, % hkfn, % state
Hotkey, *WheelDown, % hkfn, % state
Hotkey, *XButton1, % hkfn, % state
Hotkey, *XButton2, % hkfn, % state
if (state="On") ; Disables all mouse movement when "On"
BlockInput, MouseMove
else
BlockInput, MouseMoveOff
return
}
; Cycle cursors.
cycleCursor(state){
if !state ; Manage hotkey up event - do nothing.
return
if this.GuiControls.cycleCursorCB.Get() ; Only cycle same id
this.cycleSame()
else
this.cycleAll()
}
cycleSame(){
for k, cur in Cursor.AllCursors
if (cur.selectedMouse!=this.selectedMouse) { ; Wrong id, continue
continue
} else if (cur.toggleState) { ; Turn off the first one that is on
cur.toggle(1), oneOff:=1
} else if (oneOff) { ; Now we can turn on the next
cur.toggle(1), oneOn:=1
break
}
if !oneOn ; No cursor was turned on, turn on the first with correct id.
for k, cur in Cursor.AllCursors
if (cur.selectedMouse=this.selectedMouse) {
cur.toggle(1)
break
}
return
}
cycleAll(){
; Find the next cursor, turn it on
for k, cur in Cursor.AllCursors
if (cur.toggleState) { ; Turn off the first one that is on
cur.toggle(1), oneOff:=1
} else if (oneOff) {
cur.toggle(1), oneOn:=1 ; Turn on the next one
break
}
if !oneOn
Cursor.AllCursors[1].toggle(1) ; No cursor turned on, turn on the first one.
return
}
MoveCursors(MouseID, dx, dy, usButtonFlags, usButtonData){
; Move all cursors for the MouseId that generated the movement, dx,dy.
; Confine to (x,y) ∈ [cur.MinW,cur.MaxW]x[cur.MinH,cur.MaxH]
; Note: hide/show/move Cursor functions refer to the cursor GUI representation, not the actual (windows) cursor.
Cursor.md.SetState(0)
SetWinDelay,-1
SetMouseDelay,-1
CoordMode, Mouse, Screen
idCtr:=Cursor.MouseIdCounter[MouseID]
for k, cur in Cursor.AllCursors ; Consider all cursors. each "cur" is one instances of the plugin.
if (cur.toggleState && cur.SelectedMouse=MouseID) {
cur.x+=dx?dx*cur.sens:0 ; Update position
cur.y+=dy?dy*cur.sens:0
cur.x:= cur.x>cur.MaxX?cur.MaxX:(cur.xcur.MaxY?cur.MaxY:(cur.y0 ? "WheelUp" : "WheelDown") . "}"
return
}
LoadCursor(){
; Url https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391%28v=vs.85%29.aspx
static cursorIds:={ IDC_ARROW :32512
, IDC_IBEAM :32513
, IDC_WAIT :32514
, IDC_CROSS :32515
, IDC_UPARROW :32516
, IDC_SIZE :32640
, IDC_ICON :32641
, IDC_SIZENWSE :32642
, IDC_SIZENESW :32643
, IDC_SIZEWE :32644
, IDC_SIZENS :32645
, IDC_SIZEALL :32646
, IDC_NO :32648
, IDC_HAND :32649
, IDC_APPSTARTING :32650
, IDC_HELP :32651 }
this.hIcons:={}
for IDC, id in cursorIds
this.hIcons[SubStr(IDC,5)]:=DllCall("LoadCursor", "Uint", 0, "Uint", id)
return
}
GetCursorHotspot(CursorStyle:=""){
; Url :
; - https://msdn.microsoft.com/en-us/library/windows/desktop/ms648389%28v=vs.85%29.aspx (GetCursorInfo)
; - https://msdn.microsoft.com/en-us/library/windows/desktop/ms648381(v=vs.85).aspx (Cursorinfo struct)
; struct cursorinfo {
; DWORD cbSize;
; DWORD flags;
; HCURSOR hCursor;
; POINT ptScreenPos;
; }
; Url:
; - https://msdn.microsoft.com/en-us/library/windows/desktop/ms648070(v=vs.85).aspx (GetIconInfo)
; - https://msdn.microsoft.com/en-us/library/windows/desktop/ms648052(v=vs.85).aspx (Iconinfo struct)
;
; struct _ICONINFO {
; BOOL fIcon; fIcon := NumGet(&PICONINFO, 0,"Int")
; DWORD xHotspot;
; DWORD yHotspot;
; HBITMAP hbmMask; hBMMask := NumGet(&PICONINFO,12,"UPtr")
; HBITMAP hbmColor; BMColor := NumGet(&PICONINFO,12+A_PtrSize,"UPtr")
; }
;
; Get Cursor handle
static hotspots:={}
if !CursorStyle
CursorStyle:=A_Cursor
if hotspots.Haskey(CursorStyle)
return hotspots[CursorStyle]
VarSetCapacity(pci, (cbSize:=16+A_PtrSize), 0)
NumPut(cbSize,&pci,"Uint")
DllCall("GetCursorInfo", "UPtr", &pci)
hCursor := NumGet(pci, 8, "UPtr")
; Get icon info - cursor hotspot
VarSetCapacity(PICONINFO, 12+2*A_PtrSize, 0)
DllCall("GetIconInfo", "Ptr", hCursor, "Ptr", &PICONINFO)
xHotspot := NumGet(&PICONINFO, 4,"UInt")
yHotspot := NumGet(&PICONINFO, 8,"UInt")
hs:=[xHotspot?xHotspot:0, yHotspot?yHotspot:0]
hotspots[CursorStyle]:=hs
return hs
}
; Credits, Class MouseDelta: https://autohotkey.com/boards/viewtopic.php?f=19&t=10159
; This version is slightly modified.
Class MouseDelta {
State := 0
__New(callback){
this.MouseMovedFn := this.MouseMoved.Bind(this)
this.Callback := callback
}
Start(){
static DevSize := 8 + A_PtrSize, RIDEV_INPUTSINK := 0x00000100
; Register mouse for WM_INPUT messages.
VarSetCapacity(RAWINPUTDEVICE, DevSize)
NumPut(1, RAWINPUTDEVICE, 0, "UShort")
NumPut(2, RAWINPUTDEVICE, 2, "UShort")
NumPut(RIDEV_INPUTSINK, RAWINPUTDEVICE, 4, "Uint")
; WM_INPUT needs a hwnd to route to, so get the hwnd of the AHK Gui.
; It doesn't matter if the GUI is showing, it still exists
Gui +hwndhwnd
NumPut(hwnd, RAWINPUTDEVICE, 8, "Uint")
this.RAWINPUTDEVICE := RAWINPUTDEVICE
DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize )
OnMessage(0x00FF, this.MouseMovedFn)
this.State := 1
return this ; allow chaining
}
Stop(){
static RIDEV_REMOVE := 0x00000001
static DevSize := 8 + A_PtrSize
OnMessage(0x00FF, this.MouseMovedFn, 0)
RAWINPUTDEVICE := this.RAWINPUTDEVICE
NumPut(RIDEV_REMOVE, RAWINPUTDEVICE, 4, "Uint")
DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize )
this.State := 0
return this ; allow chaining
}
SetState(state){
if (state && !this.State)
this.Start()
else if (!state && this.State)
this.Stop()
return this ; allow chaining
}
Delete(){
this.Stop()
this.MouseMovedFn := ""
}
; Called when the mouse moved.
; Messages tend to contain small (+/- 1) movements, and happen frequently (~20ms)
MouseMoved(wParam, lParam){
Critical
; RawInput statics
static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, pcbSize:=8+2*A_PtrSize, offsets := {usButtonFlags:12+A_PtrSize*2, usButtonData:14+A_PtrSize*2, x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput
; Get hDevice from RAWINPUTHEADER to identify which mouse this data came from
VarSetCapacity(header, pcbSize, 0)
If (!DllCall("GetRawInputData", "UPtr", lParam, "uint", 0x10000005, "UPtr", &header, "Uint*", pcbSize, "Uint", pcbSize) or ErrorLevel)
Return 0
if !(ThisMouse := NumGet(header, 8, "UPtr"))
return
; Find size of rawinput data - only needs to be run the first time.
if (!iSize){
r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", iSize, "UInt", 8 + (A_PtrSize * 2))
VarSetCapacity(uRawInput, iSize)
}
sz := iSize ; param gets overwritten with # of bytes output, so preserve iSize
; Get RawInput data
r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", &uRawInput, "UInt*", sz, "UInt", 8 + (A_PtrSize * 2))
; Ensure we always report a number for an axis. Needed? - No it is overwritten by the numget.
dx := NumGet(&uRawInput, offsets.x, "Int")
dy := NumGet(&uRawInput, offsets.y, "Int")
usButtonFlags:=NumGet(&uRawInput, offsets.usButtonFlags, "UShort") ; Transition state of the mouse buttons.
usButtonData:=NumGet(&uRawInput, offsets.usButtonData, "Short") ; Contains mouse wheel delta, if usButtonFlags=RI_MOUSE_WHEEL=0x0400
this.Callback.(ThisMouse, dx, dy, usButtonFlags, usButtonData)
}
}
}