; 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) } } }