├── pic ├── demo.gif └── recorder.jpg ├── LICENSE ├── demo.ahk ├── README.md ├── GestureRecorder.ahk └── HotGestures.ahk /pic/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tebayaki/HotGestures/HEAD/pic/demo.gif -------------------------------------------------------------------------------- /pic/recorder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tebayaki/HotGestures/HEAD/pic/recorder.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tebayaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo.ahk: -------------------------------------------------------------------------------- 1 | #Include 2 | 3 | leftSlide := HotGestures.Gesture("←:-1,0") 4 | rightSlide := HotGestures.Gesture("→:1,0") 5 | circle := HotGestures.Gesture("O:-20,0|-20,3|-19,5|-19,7|-18,10|-16,12|-15,14|-13,15|-11,17|-9,18|-6,19|-4,20|-1,20|1,20|4,20|6,19|9,18|11,17|13,15|15,14|16,12|18,10|19,7|19,5|20,3|20,0|20,-3|19,-5|19,-7|18,-10|16,-12|15,-14|13,-15|11,-17|9,-18|6,-19|4,-20|1,-20|-1,-20|-4,-20|-6,-19|-9,-18|-11,-17|-13,-15|-15,-14|-16,-12|-18,-10|-19,-7|-19,-5|-20,-3") 6 | z := HotGestures.Gesture("Z:2,0|4,-1|8,0|10,0|14,0|17,-1|18,-2|18,-1|19,-1|17,-1|16,0|15,0|12,-1|11,-1|12,0|13,0|12,0|13,0|10,0|6,0|4,0|1,0|-1,1|-3,2|-5,2|-5,4|-7,5|-8,6|-10,6|-12,7|-13,9|-15,8|-14,8|-14,9|-13,7|-12,7|-12,7|-11,6|-12,7|-13,7|-13,7|-12,5|-11,5|-9,4|-8,4|-6,3|-4,2|-1,1|2,0|5,0|9,0|12,0|16,0|19,0|23,0|27,0|34,0|35,1|33,1|31,0|25,0|19,0|13,0|8,0") 7 | s := HotGestures.Gesture("S:0,-1|0,-1|-1,-1|-1,-1|-2,-1|-1,-1|-1,-1|-1,0|-2,-1|-2,-2|-3,-1|-4,-2|-5,-1|-7,-1|-7,0|-9,0|-8,0|-9,0|-8,0|-6,0|-4,2|-4,1|-4,2|-4,3|-4,3|-5,3|-5,3|-4,3|-4,4|-4,4|-5,5|-4,5|-3,6|-1,7|-1,7|0,8|0,9|2,8|2,7|3,5|3,4|5,4|7,4|9,4|11,5|13,6|14,5|13,5|15,5|14,5|11,5|10,4|7,3|5,2|3,2|2,2|1,2|1,1|1,1|0,2|0,2|0,3|0,4|0,3|-2,4|-2,5|-3,4|-5,5|-6,6|-7,5|-8,6|-10,6|-11,6|-12,5|-11,4|-12,4|-10,4|-7,2|-9,1|-8,1|-8,1|-6,1|-5,0|-3,0|-2,0") 8 | 9 | hgs := HotGestures() 10 | hgs.Register(leftSlide, "Backspace", _ => Send("{BackSpace}")) 11 | hgs.Register(rightSlide, "Wrap", _ => Send("{Enter}")) 12 | hgs.Register(circle, "Select All", _ => Send("^a")) 13 | hgs.Register(z, "Undo", _ => Send("^z")) 14 | hgs.Register(s, "Save", _ => Send("^s")) 15 | 16 | HotIfWinactive("ahk_class Notepad") 17 | hgs.Hotkey("RButton") 18 | HotIfWinactive() 19 | 20 | txt := " 21 | ( 22 | click and hold the right button to gesture: 23 | left slide: backspace 24 | right slide: wrap 25 | circle: select all 26 | z: undo 27 | s: save 28 | )" 29 | Run("Notepad.exe") 30 | hwnd := WinWaitActive("ahk_class Notepad") 31 | Sleep(1000) 32 | try { 33 | ControlSetText(txt, "RichEditD2DPT1", hwnd) 34 | } 35 | catch { 36 | try { 37 | ControlSetText(txt, "Edit1", hwnd) 38 | } 39 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HotGestures 2 | This is a light Autohotkey library for mouse gestures creating and recognizing. It helps you to customize any one-stroke pattern and bind it to any function. 3 | It can accurately recognize complex patterns, including straight lines, arcs, and polylines. So you can draw L, O, M, U, Z, and even a star shape on the screen to do whatever you want. 4 | ![demo](./pic/demo.gif) 5 | ## How it works 6 | HotGestures recognizes mouse gestures by measuring the similarity of two sets of vectors through the DTW(Dynamic Time Warping). What we need to do is to provide several sets of vectors describing the mouse gestures as standard patterns. When we do a gesture, it compares the current mouse track with all the saved patterns and selects the closest one from the matched patterns. 7 | ## Vectors generation 8 | A gesture can be seen as a set of vectors. HotGestures saves and parses string describing vectors in this format: `[NAME:]X1,Y1[|X2,Y2]...` 9 | #### For example: 10 | ``` 11 | Right-slide:10,0|10,0|10,0 12 | 13 | Left-slide:-10,0|-10,0|-10,0 14 | 15 | Circle:-20,0|-20,3|-19,5|-19,7|-18,10|-16,12|-15,14|-13,15|-11,17|-9,18|-6,19|-4,20|-1,20|1,20|4,20|6,19|9,18|11,17|13,15|15,14|16,12|18,10|19,7|19,5|20,3|20,0|20,-3|19,-5|19,-7|18,-10|16,-12|15,-14|13,-15|11,-17|9,-18|6,-19|4,-20|1,-20|-1,-20|-4,-20|-6,-19|-9,-18|-11,-17|-13,-15|-15,-14|-16,-12|-18,-10|-19,-7|-19,-5|-20,-3 16 | ``` 17 | Manually writing of these is a bit of an imposition. Fortunately, you can use the GestureRecorder to generate them. 18 | ![Gesture Recorder](./pic/recorder.jpg) 19 | Start GestureRecorder, draw gestures on the whiteboard with the left button, and then you will see the vectors in the listview, just select it and press Ctrl-C to copy it. 20 | When you are done with all the gestures recording and naming, click the "Generate Code" button and you'll get a code template. 21 | 22 | ## Coding 23 | #### This example shows how to create and register gestures. 24 | ```autohotkey 25 | leftSlide := HotGestures.Gesture("Left-slide:-1,0") ; Create a left slide gesture 26 | rightSlide := HotGestures.Gesture("Right-slide:1,0") ; Create a right slide gesture 27 | hgs := HotGestures() ; New a HotGestures instance 28 | ; hgs.Register method requires a HotGestures.Gesture object, a string as comment, and a function as callback. 29 | hgs.Register(leftSlide, "Switch window", _ => Send("!{Tab}")) 30 | hgs.Register(rightSlide, "Next Window", _ => Send("+!{Tab}")) 31 | hgs.Hotkey("RButton") ; set the RButton as the trigger 32 | ``` 33 | #### This demo creates two gesture groups containing identical gestures. But they serve different purposes in Notepad and Explorer windows, respectively. 34 | ```autohotkey 35 | leftSlide := HotGestures.Gesture("Left-slide:-1,0") 36 | rightSlide := HotGestures.Gesture("Right-slide:1,0") 37 | 38 | notepadHgs := HotGestures() 39 | notepadHgs.Register(leftSlide, "Backspace", _ => Send("{BackSpace}")) 40 | notepadHgs.Register(rightSlide, "Wrap", _ => Send("{Enter}")) 41 | 42 | explorerHgs := HotGestures() 43 | explorerHgs.Register(leftSlide, "Back", _ => Send("!{Left}")) 44 | explorerHgs.Register(rightSlide, "Forward", _ => Send("!{Right}")) 45 | 46 | ; use Hotif to create context-sensitive gesture trigger 47 | HotIfWinactive("ahk_class Notepad") 48 | notepadHgs.Hotkey("RButton") 49 | HotIfWinactive("ahk_class CabinetWClass ahk_exe explorer.exe") 50 | explorerHgs.Hotkey("RButton") 51 | HotIfWinactive() 52 | ``` 53 | #### This example is a little more complex than the 2 above. Comparatively, this approach allows you to start or end gesture recording at any time, as well as control what to do in case of no match. 54 | ```autohotkey 55 | ; This is a M-Shape gesture 56 | nShape := HotGestures.Gesture("M-Shape:0,-2|2,-4|3,-7|3,-9|3,-12|5,-14|5,-17|5,-20|6,-20|5,-22|5,-22|6,-24|5,-17|3,-10|1,-5|2,-3|3,2|5,11|8,21|9,22|9,29|7,23|5,21|5,18|5,12|3,8|1,5|1,3|1,2|1,-1|3,-5|4,-10|6,-10|8,-16|8,-14|10,-21|8,-18|5,-16|6,-19|5,-16|4,-15|2,-8|2,-4|1,-2|1,-1|1,0|2,2|2,4|3,9|3,12|6,16|9,30|11,29|10,28|8,24|6,19|3,15|3,11|1,8|1,6|0,5|0,3 57 | ") 58 | ; This is a Z-Shape gesture 59 | zShape := HotGestures.Gesture("Z-Shape:0,-1|0,-1|6,-1|13,-1|16,-1|21,-1|21,-2|21,-1|14,0|11,0|7,0|3,1|2,1|-2,3|-5,4|-7,6|-18,11|-18,10|-19,9|-20,9|-31,12|-22,9|-18,9|-8,5|-1,2|6,1|15,-1|20,-1|25,-2|25,-3|24,-3|19,-1|15,-1|10,0|5,0|3,0|2,0") 60 | 61 | hgs := HotGestures() 62 | hgs.Register(nShape, "Maximize/Restore current window") 63 | hgs.Register(zShape, "Minimize current window") 64 | 65 | $RButton::{ 66 | hgs.Start() ; Start recording 67 | KeyWait("RButton") ; Keep recording until RButton is released 68 | hgs.Stop() ; Stop recording 69 | if hgs.Result.Valid { ; Check validity of result 70 | switch hgs.Result.MatchedGesture { ; hgs.Result.MatchedGesture is the matched gesture object 71 | case nShape: try WinGetMinMax(WinExist("A")) ? WinRestore() : WinMaximize() 72 | case zShape: try WinMinimize("A") 73 | case "": return ; hgs.Result.MatchedGesture is an empty string if no match 74 | } 75 | } 76 | ; if no movement or track is too short, hgs.Result.Valid is false, and a right click is expected 77 | else { 78 | Send("{RButton}") 79 | } 80 | } 81 | ``` 82 | #### Parameters: 83 | - maxDistance indicates the maximum distance (0~2), the smaller the value, the stricter the recognition. 84 | - minTrackLen indicates the shortest track length required in pixels, if the length of the mouse track is less than this value, the gesture is considered invalid. 85 | - penColor indicates the color of the track. 86 | ```autohotkey 87 | hgs := HotGestures(maxDistance := 0.1, minTrackLen := 100, penColor := 0xFE4F7F) 88 | ``` -------------------------------------------------------------------------------- /GestureRecorder.ahk: -------------------------------------------------------------------------------- 1 | #Include HotGestures.ahk 2 | GestureRecorder() 3 | 4 | GestureRecorder() { 5 | hgs := HotGestures() 6 | testing := false 7 | animation := "" 8 | itemIndex := 1 9 | maxDistance := 0.1 10 | 11 | mainWindow := Gui(, "Gesture Recorder") 12 | mainWindow.SetFont(, "Cascadia Code") 13 | mainWindow.AddText(, "F2: edit name | Ctrl+C: copy vectors") 14 | addBtn := mainWindow.AddButton("Section w120", "Add") 15 | deleteBtn := mainWindow.AddButton("x+M wp", "Delete") 16 | genCodeBtn := mainWindow.AddButton("x+M wp", "Generate Code") 17 | testBtn := mainWindow.AddButton("x+M wp", "Test") 18 | mainWindow.AddText("x+M wp hp-5 Center 0x200", "Max Distance:") 19 | distanceEdit := mainWindow.AddEdit("x+M w100", "0.1") 20 | listview := mainWindow.AddListView("-ReadOnly Center Grid xs w500 h500", ["Name", "Vectors"]) 21 | drawingBoard := mainWindow.AddText("BackgroundWhite Center w500 h500 x+M") 22 | statusBar := mainWindow.AddStatusBar() 23 | 24 | listview.ModifyCol(1, 100) 25 | mainWindow.OnEvent("Close", MainWindow_Close) 26 | addBtn.OnEvent("Click", AddButton_Click) 27 | deleteBtn.OnEvent("Click", DeleteBtn_Click) 28 | genCodeBtn.OnEvent("Click", GenCodeBtn_Click) 29 | testBtn.OnEvent("Click", TestBtn_Click) 30 | drawingBoard.OnEvent("Click", DrawingBoard_Click) 31 | listview.OnEvent("ItemFocus", ListView_ItemFocus) 32 | ; listview.OnEvent("ItemSelect", ListView_ItemSelect) 33 | OnMessage(0x0100, ListView_DeleteKeyDown) 34 | OnMessage(0x0102, ListView_CtrlC) 35 | mainWindow.Show() 36 | 37 | InitDrawingBoard() 38 | listview.Add(, "↑", "0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10|0,-10") 39 | listview.Add(, "↓", "0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10|0,10") 40 | listview.Add(, "←", "-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0|-10,0") 41 | listview.Add(, "→", "10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0|10,0") 42 | listview.Add(, "circle", "-20,0|-20,3|-19,5|-19,7|-18,10|-16,12|-15,14|-13,15|-11,17|-9,18|-6,19|-4,20|-1,20|1,20|4,20|6,19|9,18|11,17|13,15|15,14|16,12|18,10|19,7|19,5|20,3|20,0|20,-3|19,-5|19,-7|18,-10|16,-12|15,-14|13,-15|11,-17|9,-18|6,-19|4,-20|1,-20|-1,-20|-4,-20|-6,-19|-9,-18|-11,-17|-13,-15|-15,-14|-16,-12|-18,-10|-19,-7|-19,-5|-20,-3") 43 | ControlFocus(listview) 44 | 45 | MainWindow_Close(mainWindow) { 46 | animation := "" 47 | mainWindow.Destroy() 48 | } 49 | 50 | AddButton_Click(ctrl, info) { 51 | dialog := Gui("Owner" mainWindow.Hwnd, "Add a gesture") 52 | dialog.SetFont(, "Cascadia Code") 53 | dialog.AddText(, "Vectors:") 54 | vectorsEdit := dialog.AddEdit("w400 r8") 55 | okBtn := dialog.AddButton("w190", "OK") 56 | cancelBtn := dialog.AddButton("wp x+M", "Cancel") 57 | dialog.Show() 58 | okBtn.OnEvent("Click", OkButton_Click) 59 | cancelBtn.OnEvent("Click", (ctrl, info) => dialog.Destroy()) 60 | 61 | OkButton_Click(ctrl, info) { 62 | try 63 | gesture := HotGestures.Gesture(Trim(vectorsEdit.Text, " `r`n`t")) 64 | catch 65 | MsgBox("Invalid vectors", "Warming") 66 | else if AddGesture(gesture) 67 | dialog.Destroy() 68 | } 69 | } 70 | 71 | DeleteBtn_Click(ctrl, info) => DeleteSelections() 72 | 73 | GenCodeBtn_Click(ctrl, info) { 74 | if CheckDistance(&distance) == "" 75 | return 76 | hgsVarName := "hgs" 77 | part1 := part2 := part3 := "" 78 | loop listview.GetCount() { 79 | name := listview.GetText(A_Index, 1) 80 | vectors := listview.GetText(A_Index, 2) 81 | varName := "gesture" A_Index 82 | part1 .= Format('{} := HotGestures.Gesture("{}")`n', varName, name ":" vectors) 83 | part2 .= Format('{}.Register({}, "")`n', hgsVarName, varName) 84 | part3 .= Format('case {}: `; {}`n ', varName, name) 85 | } 86 | part3 .= "default: return" 87 | code := Format(" 88 | ( 89 | {1} 90 | {2} := HotGestures({3}) 91 | {4} 92 | $RButton::{ 93 | {2}.Start() 94 | KeyWait("RButton") 95 | {2}.Stop() 96 | if {2}.Result.Valid { 97 | switch {2}.Result.MatchedGesture { 98 | {5} 99 | } 100 | } 101 | else { 102 | Send("{RButton}") 103 | } 104 | } 105 | )", part1, hgsVarName, Format("{:g}", distance), part2, part3) 106 | codeUi := Gui("+Owner" mainWindow.Hwnd, "Gesture code") 107 | codeUi.SetFont(, "Cascadia Code") 108 | codeUi.AddEdit("Multi HScroll w800 h500", code) 109 | codeUi.Show() 110 | } 111 | 112 | TestBtn_Click(ctrl, info) { 113 | if testing { 114 | hgs.Clear() 115 | testBtn.Text := "Test" 116 | statusBar.Text := "" 117 | addBtn.Opt("-Disabled") 118 | deleteBtn.Opt("-Disabled") 119 | genCodeBtn.Opt("-Disabled") 120 | distanceEdit.Opt("-Disabled") 121 | testing := false 122 | } 123 | else { 124 | if !CheckDistance(&distance) 125 | return 126 | hgs.MaxDistance := distance 127 | loop listview.GetCount() { 128 | name := listview.GetText(A_Index, 1) 129 | vectors := listview.GetText(A_Index, 2) 130 | gestrue := HotGestures.Gesture(name ":" vectors) 131 | hgs.Register(gestrue, "") 132 | } 133 | animation := "" 134 | InitDrawingBoard() 135 | testBtn.Text := "Stop Test" 136 | statusBar.Text := "Testing" 137 | addBtn.Opt("Disabled") 138 | deleteBtn.Opt("Disabled") 139 | genCodeBtn.Opt("Disabled") 140 | distanceEdit.Opt("Disabled") 141 | testing := true 142 | } 143 | } 144 | 145 | DrawingBoard_Click(ctrl, info) { 146 | animation := "" 147 | InitDrawingBoard("") 148 | if hgs.StartAndWait("LButton", &result) { 149 | if testing { 150 | if result.MatchedGesture 151 | statusBar.Text := result.MatchedGesture.Name " matched, distance: " result.Distance 152 | else 153 | statusBar.Text := "no match" 154 | } 155 | else { 156 | newGesture := HotGestures.Gesture("Unnamed" itemIndex++, result.Vectors) 157 | AddGesture(newGesture) 158 | } 159 | } 160 | else { 161 | InitDrawingBoard() 162 | } 163 | } 164 | 165 | ListView_CtrlC(wp, lp, msg, hwnd) { 166 | if hwnd == listview.Hwnd && wp == 3 { 167 | row := 0 168 | str := "" 169 | while row := listview.GetNext(row) 170 | str .= listview.GetText(row, 1) ":" listview.GetText(row, 2) "`n" 171 | if str != "" { 172 | A_Clipboard := Trim(str) 173 | statusBar.Text := "Copied!" 174 | } 175 | } 176 | } 177 | 178 | ListView_DeleteKeyDown(wp, lp, msg, hwnd) { 179 | if hwnd == listview.Hwnd && wp == 0x2E 180 | DeleteSelections() 181 | } 182 | 183 | ListView_ItemFocus(ctrl, item) { 184 | ; if focus changed because of deleting a item, the item index passed in is incorrect. 185 | if item := listview.GetNext(0, "Focused") { 186 | name := listview.GetText(item, 1) 187 | vectorsStr := listview.GetText(item, 2) 188 | gesture := HotGestures.Gesture(name, vectorsStr) 189 | animation := CreateAnimation(gesture) 190 | } 191 | } 192 | 193 | AddGesture(newGesture) { 194 | prompt := "" 195 | for name, vectorsStr in GesturesEnumerator() { 196 | d := newGesture.Compare(HotGestures.Gesture(name ":" vectorsStr)) 197 | if d <= maxDistance { 198 | prompt .= "`n Name: " name "`tDistance: " d 199 | } 200 | } 201 | if prompt != "" { 202 | mainWindow.Opt("OwnDialogs") 203 | prompt := "This gesture is too similar to the following:" prompt "`n`nAre you sure to add it?" 204 | if "No" = MsgBox(prompt, "Warming", 0x4) 205 | return false 206 | } 207 | listview.Add("Vis Focus", newGesture.Name, newGesture.ToString()) 208 | animation := CreateAnimation(newGesture) 209 | return true 210 | } 211 | 212 | GesturesEnumerator() { 213 | return enum 214 | enum(&name, &vectors) { 215 | if A_Index > listview.GetCount() 216 | return false 217 | name := listview.GetText(A_Index, 1) 218 | vectors := listview.GetText(A_Index, 2) 219 | } 220 | } 221 | 222 | CreateAnimation(gesture, interval := 20, repeatInterval := 1000) { 223 | InitDrawingBoard("") 224 | points := [] 225 | points.Capacity := gesture.Length 226 | x := y := left := top := right := bottom := 0 227 | for v in gesture { 228 | points.Push([x += v[1], y += v[2]]) 229 | left := Min(x, left) 230 | top := Min(y, top) 231 | right := Max(x, right) 232 | bottom := Max(y, bottom) 233 | } 234 | w := right - left 235 | h := bottom - top 236 | 237 | dc := DllCall("GetDC", "ptr", drawingBoard.Hwnd, "ptr") 238 | ; transform 239 | drawingBoard.GetPos(, , &boardW, &boardH) 240 | boardW *= A_ScreenDPI / 96 241 | boardH *= A_ScreenDPI / 96 242 | xform := Buffer(24) 243 | scale := 1 244 | if w > boardH - 5 || h > boardH - 5 { 245 | scale := Min((boardW - 10) / w, (boardH - 10) / h) 246 | w *= scale, h *= scale, left *= scale, top *= scale 247 | } 248 | NumPut("float", scale, "float", 0, "float", 0, "float", scale, "float", (boardW - w) / 2 - left, "float", (boardH - h) / 2 - top, xform) 249 | DllCall("SetGraphicsMode", "ptr", dc, "int", 2) 250 | DllCall("SetWorldTransform", "ptr", dc, "ptr", xform) 251 | 252 | pen := DllCall("CreatePen", "int", 0, "int", 5, "uint", 0xFE4F7F, "ptr") 253 | DllCall("SelectObject", "ptr", dc, "ptr", pen, "ptr") 254 | 255 | index := 1 256 | SetTimer(LineToNextPoint, interval) 257 | return { __Delete: __Delete } 258 | 259 | LineToNextPoint() { 260 | if index == 0 { 261 | InitDrawingBoard("") 262 | DllCall("MoveToEx", "ptr", dc, "int", 0, "int", 0, "ptr", 0) 263 | SetTimer(LineToNextPoint, interval) 264 | index++ 265 | } 266 | if index <= points.Length { 267 | DllCall("LineTo", "ptr", dc, "int", points[index][1], "int", points[index][2]) 268 | index++ 269 | } 270 | else { 271 | SetTimer(LineToNextPoint, -repeatInterval) 272 | index := 0 273 | } 274 | } 275 | 276 | __Delete(_) { 277 | SetTimer(LineToNextPoint, 0) 278 | DllCall("DeleteObject", "ptr", pen) 279 | DllCall("ReleaseDC", "ptr", dc) 280 | } 281 | } 282 | 283 | InitDrawingBoard(text?) { 284 | text := text ?? "Draw gestures here with the left button" 285 | dc := DllCall("GetDC", "ptr", drawingBoard.Hwnd, "ptr") 286 | drawingBoard.GetPos(, , &boardW, &boardH) 287 | boardW *= A_ScreenDPI / 96 288 | boardH *= A_ScreenDPI / 96 289 | DllCall("Rectangle", "ptr", dc, "int", 0, "int", 0, "int", boardW, "int", boardH) 290 | if text { 291 | rect := Buffer(16) 292 | NumPut("int64", 0, "int", boardW, "int", boardH, rect) 293 | font := SendMessage(0x0031, 0, 0, drawingBoard) 294 | DllCall("SelectObject", "ptr", dc, "ptr", font, "ptr") 295 | DllCall("DrawTextW", "ptr", dc, "str", text, "int", StrLen(text), "ptr", rect, "uint", 0x125) 296 | } 297 | DllCall("ReleaseDC", "ptr", dc) 298 | } 299 | 300 | DeleteSelections() { 301 | if testing { 302 | statusBar.Text := "Please stop test" 303 | } 304 | else { 305 | row := 0 306 | arr := [] 307 | while row := listview.GetNext(row) 308 | arr.InsertAt(1, row) 309 | else 310 | return 311 | animation := "" 312 | for r in arr 313 | listview.Delete(r) 314 | statusBar.Text := arr.Length " items deleted" 315 | } 316 | if !listview.GetNext(0, "Focused") { 317 | InitDrawingBoard() 318 | } 319 | } 320 | 321 | CheckDistance(&distance) { 322 | distanceStr := distanceEdit.Text 323 | if IsNumber(distanceStr) { 324 | distance := Number(distanceStr) 325 | if distance >= 0 && distance <= 2 326 | return true 327 | } 328 | statusBar.Text := "invalid distance" 329 | return false 330 | } 331 | } -------------------------------------------------------------------------------- /HotGestures.ahk: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * @description An Autohotkey library for creating and recognizing any custom mouse gestures. 3 | * @author Tebayaki 4 | * @date 2023/9/30 5 | * @version 1.0 6 | ***********************************************************************/ 7 | class HotGestures { 8 | __matchInfos := Map() 9 | 10 | __New(maxDistance := 0.1, minTrackLen := 100, penColor := 0xFE4F7F) { 11 | this.MaxDistance := maxDistance 12 | this.MinTrackLength := minTrackLen 13 | this.__drawingBoard := HotGestures.DrawingBoard(penColor) 14 | } 15 | 16 | __Delete() => this.__drawingBoard.Destroy() 17 | 18 | Register(gesture, comment, callback := "") { 19 | matchInfo := { Gesture: gesture, Comment: comment, Callback: callback, Matrix: HotGestures.DistanceMatrix(gesture), Excluded: false } 20 | this.__matchInfos.Set(gesture, matchInfo) 21 | } 22 | 23 | Unregister(gesture) => this.__matchInfos.Delete(gesture) 24 | 25 | Clear() => this.__matchInfos.Clear() 26 | 27 | Hotkey(keyName, options := "") { 28 | keyName := GetKeyName(keyName) 29 | if keyName == "" 30 | throw Error("invalid key name") 31 | sendIfNoMoved := !(keyName ~= "Control|Shift|Alt|Win") 32 | Hotkey("$" keyName, OnHotkey, options) 33 | 34 | OnHotkey(_) { 35 | this.Start() 36 | KeyWait(keyName) 37 | this.Stop() 38 | if this.Result.Valid { 39 | if this.__match && this.__match.Callback is Func 40 | this.__match.Callback.Call(this.Result) 41 | } 42 | else if sendIfNoMoved { 43 | Send("{" keyName "}") 44 | } 45 | } 46 | } 47 | 48 | StartAndWait(keyName, &result) { 49 | if GetKeyName(keyName) == "" 50 | throw Error("invalid key name", , keyName) 51 | this.Start() 52 | KeyWait(keyName) 53 | this.Stop() 54 | result := this.Result 55 | return this.Result.Valid 56 | } 57 | 58 | Start() { 59 | CoordMode("Mouse", "Screen") 60 | MouseGetPos(&x, &y) 61 | this.__lastX := x 62 | this.__lastY := y 63 | this.__drawingBoard.MoveTo(x, y) 64 | this.__trackLen := 0 65 | this.__match := "" 66 | this.__vectors := [] 67 | for , mi in this.__matchInfos { 68 | mi.Excluded := false 69 | mi.Matrix.Clear() 70 | } 71 | this.Result := { Valid: false, Vectors: this.__vectors, MatchedGesture: "", Distance: "", Comment: "", } 72 | this.__mouseHook := HotGestures.MouseHook(this.__OnMouseMove.Bind(this)) 73 | this.__drawingBoard.Show() 74 | } 75 | 76 | Stop() { 77 | this.__mouseHook := "" 78 | if this.__trackLen >= this.MinTrackLength { 79 | this.Result.Valid := true 80 | if this.__match { 81 | this.Result.MatchedGesture := this.__match.Gesture 82 | this.Result.Distance := this.__match.Distance 83 | this.Result.Comment := this.__match.Comment 84 | } 85 | } 86 | this.__drawingBoard.Hide() 87 | } 88 | 89 | __OnMouseMove(x, y) { 90 | ; anti-shake 91 | x := (x + this.__lastX) // 2 92 | y := (y + this.__lastY) // 2 93 | if x == this.__lastX && y == this.__lastY 94 | return 95 | this.__drawingBoard.DrawLineTo(x, y) 96 | 97 | vx := x - this.__lastX 98 | vy := y - this.__lastY 99 | newVector := [vx, vy] 100 | this.__vectors.Push(newVector) 101 | this.__BuildMatrixes(newVector) 102 | 103 | if this.__trackLen < this.MinTrackLength 104 | this.__trackLen += Sqrt(vx * vx + vy * vy) 105 | else if this.__UpdateMatch() { 106 | tip := "" 107 | if this.__match { 108 | name := this.__match.Gesture.Name 109 | comment := this.__match.Comment 110 | if name != "" && comment != "" 111 | tip := name ": " comment 112 | else if name == "" 113 | tip := comment 114 | else if comment == "" 115 | tip := name 116 | } 117 | this.__drawingBoard.DrawTip(tip) 118 | } 119 | 120 | this.__lastX := x 121 | this.__lastY := y 122 | } 123 | 124 | __BuildMatrixes(newVector) { 125 | for , mi in this.__matchInfos 126 | if !mi.Excluded 127 | mi.Matrix.Append(newVector) 128 | } 129 | 130 | __UpdateMatch() { 131 | match := "" 132 | minDist := this.MaxDistance 133 | for , mi in this.__matchInfos { 134 | distance := mi.Matrix.TraceBack() 135 | if distance <= minDist { 136 | mi.Distance := minDist := distance 137 | match := mi 138 | } 139 | else if distance > 1 { 140 | mi.Excluded := true 141 | } 142 | } 143 | if this.__match == match 144 | return false 145 | this.__match := match 146 | return true 147 | } 148 | 149 | class MouseHook { 150 | __New(function) { 151 | this.__proc := CallbackCreate(LowLevelMouseHookProc, "F") 152 | this.__hook := DllCall("SetWindowsHookEx", "int", 14, "ptr", this.__proc, "ptr", 0, "uint", 0, "ptr") 153 | 154 | LowLevelMouseHookProc(nCode, wParam, lParam) { 155 | if nCode == 0 && wParam == 0x0200 156 | function(NumGet(lParam, "int"), NumGet(lParam, 4, "int")) 157 | return DllCall("CallNextHookEx", "ptr", 0, "int", nCode, "ptr", wParam, "ptr", lParam) 158 | } 159 | } 160 | 161 | __Delete() { 162 | DllCall("UnhookWindowsHookEx", "ptr", this.__hook) 163 | CallbackFree(this.__proc) 164 | } 165 | } 166 | 167 | class DrawingBoard extends Gui { 168 | __lastTipRect := 0 169 | __lastTipX := 0 170 | __lastTipY := 0 171 | __lastTipW := 0 172 | __lastTipH := 0 173 | 174 | __New(penColor) { 175 | super.__New("+LastFound +AlwaysOnTop +ToolWindow +E0x00000020 -Caption -DPIScale") 176 | this.BackColor := 0 177 | WinSetTransColor(0) 178 | this.SetFont("S30", "Microsoft YaHei") 179 | 180 | this.__dc := DllCall("GetDC", "ptr", this.Hwnd, "ptr") 181 | this.__pen := DllCall("CreatePen", "int", 0, "int", 5, "int", penColor, "ptr") 182 | DllCall("SelectObject", "ptr", this.__dc, "ptr", this.__pen) 183 | 184 | this.__tip := this.AddText("Background0 +E0x00080000 x0 y0 w" A_ScreenWidth " h" A_ScreenHeight - 1) 185 | WinSetTransColor("0 200", this.__tip) 186 | 187 | this.__tipDC := DllCall("GetDC", "ptr", this.__tip.Hwnd, "ptr") 188 | this.__tipMemDC := DllCall("CreateCompatibleDC", "ptr", this.__tipDC, "ptr") 189 | tipBmp := DllCall("CreateCompatibleBitmap", "ptr", this.__tipDC, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr") 190 | DllCall("SelectObject", "ptr", this.__tipMemDC, "ptr", tipBmp) 191 | 192 | this.__blackBrush := DllCall("GetStockObject", "int", 4, "ptr") 193 | grayBrush := DllCall("GetStockObject", "int", 3, "ptr") 194 | DllCall("SelectObject", "ptr", this.__tipMemDC, "ptr", grayBrush) 195 | 196 | font := SendMessage(0x0031, 0, 0, this.__tip) 197 | DllCall("SelectObject", "ptr", this.__tipMemDC, "ptr", font) 198 | DllCall("SetBkMode", "ptr", this.__tipMemDC, "int", 1) 199 | DllCall("SetTextColor", "ptr", this.__tipMemDC, "uint", 0xffffff) 200 | 201 | DllCall("DeleteObject", "ptr", tipBmp) 202 | } 203 | 204 | __Delete() { 205 | DllCall("DeleteObject", "ptr", this.__pen) 206 | DllCall("DeleteDC", "ptr", this.__tipMemDC) 207 | DllCall("ReleaseDC", "ptr", this.__tipDC) 208 | DllCall("ReleaseDC", "ptr", this.__dc) 209 | } 210 | 211 | Show() { 212 | this.Opt("AlwaysOnTop") 213 | ; A_ScreenHeight - 1 to avoid "do not disturb" mode 214 | super.Show("NoActivate x0 y0 w" A_ScreenWidth " h" A_ScreenHeight - 1) 215 | } 216 | 217 | Hide() { 218 | ; Built-in WinRedraw don't redraw immediately. 219 | DllCall("RedrawWindow", "ptr", this.__tip.Hwnd, "ptr", 0, "ptr", 0, "uint", 0x205) 220 | DllCall("RedrawWindow", "ptr", this.Hwnd, "ptr", 0, "ptr", 0, "uint", 0x105) 221 | super.Hide() 222 | } 223 | 224 | MoveTo(x, y) => DllCall("MoveToEx", "ptr", this.__dc, "int", x, "int", y, "ptr", 0) 225 | 226 | DrawLineTo(x, y) => DllCall("LineTo", "ptr", this.__dc, "int", x, "int", y) 227 | 228 | DrawTip(text) { 229 | if text == "" { 230 | DllCall("RedrawWindow", "ptr", this.__tip.Hwnd, "ptr", 0, "ptr", 0, "uint", 0x105) 231 | return 232 | } 233 | ; calc text size 234 | DllCall("GetTextExtentPoint32", "ptr", this.__tipMemDC, "str", text, "int", StrLen(text), "int64*", &size := 0) 235 | w := (size & 0xffffffff) + 100 236 | h := (size >> 32) + 10 237 | NumPut("int", left := (A_ScreenWidth - w) // 2, 238 | "int", top := 200, 239 | "int", right := left + w, 240 | "int", bottom := top + h, 241 | rect := Buffer(16)) 242 | ; erase last tip rect 243 | if w < this.__lastTipW { 244 | DllCall("FillRect", "ptr", this.__tipMemDC, "ptr", this.__lastTipRect, "ptr", this.__blackBrush) 245 | DllCall("RoundRect", "ptr", this.__tipMemDC, "int", left, "int", top, "int", right, "int", bottom, "int", 20, "int", 20) 246 | DllCall("DrawTextW", "ptr", this.__tipMemDC, "str", text, "int", StrLen(text), "ptr", rect, "uint", 0x125) 247 | DllCall("BitBlt", "ptr", this.__tipDC, "int", this.__lastTipX, "int", this.__lastTipY, "int", this.__lastTipW, "int", this.__lastTipH, "ptr", this.__tipMemDC, "int", this.__lastTipX, "int", this.__lastTipY, "uint", 0x00CC0020) 248 | } 249 | else { 250 | NumPut("int", left, "int", top, "int", right, "int", bottom, rect) 251 | DllCall("RoundRect", "ptr", this.__tipMemDC, "int", left, "int", top, "int", right, "int", bottom, "int", 20, "int", 20) 252 | DllCall("DrawTextW", "ptr", this.__tipMemDC, "str", text, "int", StrLen(text), "ptr", rect, "uint", 0x125) 253 | DllCall("BitBlt", "ptr", this.__tipDC, "int", left, "int", top, "int", w, "int", h, "ptr", this.__tipMemDC, "int", left, "int", top, "uint", 0x00CC0020) 254 | } 255 | this.__lastTipRect := rect 256 | this.__lastTipX := left 257 | this.__lastTipY := top 258 | this.__lastTipW := w 259 | this.__lastTipH := h 260 | } 261 | } 262 | 263 | class Gesture extends Array { 264 | __New(name, vectors?) { 265 | valid := false 266 | if IsSet(vectors) { 267 | if vectors is String { 268 | valid := HotGestures.Gesture.VerifyVectorsString(vectors, , &vectorsArr) 269 | } 270 | else if vectors is Array { 271 | valid := HotGestures.Gesture.VerifyVectorsArray(vectors, &vectorsArr) 272 | } 273 | } 274 | else { 275 | valid := HotGestures.Gesture.VerifyVectorsString(name, &name, &vectorsArr) 276 | } 277 | if !valid { 278 | throw Error("invalid parameter(s)") 279 | } 280 | this.Name := name 281 | super.__New(vectorsArr*) 282 | } 283 | 284 | ToString() { 285 | vectorsStr := "" 286 | for v in this 287 | vectorsStr .= (A_Index == 1 ? "" : "|") v[1] "," v[2] 288 | return vectorsStr 289 | } 290 | 291 | Compare(vectors) { 292 | matrix := HotGestures.DistanceMatrix(this) 293 | for v in vectors 294 | matrix.Append(v) 295 | return matrix.TraceBack() 296 | } 297 | 298 | static VerifyVectorsArray(vectorsArr, ©) { 299 | copy := [] 300 | for v in vectorsArr { 301 | if v.Length != 2 302 | return false 303 | if !(v[1] is Integer) || !(v[2] is Integer) 304 | return false 305 | copy.Push([v[1], v[2]]) 306 | } 307 | return true 308 | } 309 | 310 | static VerifyVectorsString(vectorsStr, &name?, &vectorsArr?) { 311 | if res := RegExMatch(vectorsStr, "^(?:(.*):)?((?:-?\d+,-?\d+)(?:\|-?\d+,-?\d+)*)$", &m) { 312 | name := m[1] 313 | vectorsArr := [] 314 | loop parse m[2], "|" { 315 | v := StrSplit(A_LoopField, ",", , 2) 316 | vectorsArr.Push([Integer(v[1]), Integer(v[2])]) 317 | } 318 | } 319 | return !!res 320 | } 321 | } 322 | 323 | class DistanceMatrix extends Array { 324 | __New(standard) { 325 | static INF := NumGet(ObjPtr(&_ := 0x7F800000) + A_PtrSize * 2, "float") 326 | 327 | if !len := standard.Length 328 | throw Error("invalid parameter") 329 | 330 | this.Standard := standard 331 | this.Capacity := len + 1 332 | 333 | this.RowTemplate := [] 334 | this.RowTemplate.Capacity := len + 1 335 | this.RowTemplate.Push(INF) 336 | 337 | this.Push(firstRow := this.RowTemplate.Clone()) 338 | firstRow[1] := 0 339 | loop len 340 | firstRow.Push(INF) 341 | } 342 | 343 | Append(newVector) { 344 | static Distance(a, b) => 1 - (a[1] * b[1] + a[2] * b[2]) / (Sqrt(a[1] ** 2 + a[2] ** 2) * Sqrt(b[1] ** 2 + b[2] ** 2)) 345 | 346 | standard := this.Standard 347 | lastRow := this[-1] 348 | 349 | this.Push(newRow := this.RowTemplate.Clone()) 350 | loop standard.Length 351 | newRow.Push(Distance(newVector, standard[A_Index]) + Min(newRow[A_Index], lastRow[A_Index], lastRow[A_Index + 1])) 352 | } 353 | 354 | TraceBack() { 355 | i := this.Length - 1 , j := this[1].Length - 1 , count := 1 356 | while i > 1 && j > 1 { 357 | switch min(a := this[i][j], b := this[i][j + 1], this[i + 1][j]) { 358 | case a: i--, j-- 359 | case b: i-- 360 | default: j-- 361 | } 362 | count++ 363 | } 364 | return this[-1][-1] / (count + i + j - 2) 365 | } 366 | 367 | Clear() => this.Length := 1 368 | } 369 | } --------------------------------------------------------------------------------