├── .gitmodules ├── Examples ├── Array_examples.ahk ├── BufferedHotstrings_examples.ahk ├── Media │ ├── Example1_CurrentSession.ahk │ ├── Example2_PlaybackChangedEvent.ahk │ ├── Example3_GetThumbnail.ahk │ └── Example4_SessionChangedEvent.ahk ├── Misc_examples.ahk ├── String_examples.ahk ├── WinEvent │ ├── Example1_NotepadCreated.ahk │ ├── Example2_WindowClose.ahk │ ├── Example3_WindowMaximized.ahk │ ├── Example4_ActiveWindowChanged.ahk │ └── Example5_DockGui.ahk └── XHotstring_example.ahk ├── LICENSE ├── Lib ├── Array.ahk ├── BufferedHotstrings.ahk ├── DPI.ahk ├── DUnit.ahk ├── Editable.ahk ├── FindTextDpi.ahk ├── JAB.ahk ├── Map.ahk ├── Media.ahk ├── Misc.ahk ├── String.ahk ├── Sync.ahk ├── WinEvent.ahk └── XHotstring.ahk ├── README.md ├── Resources └── DPI_Tutorial │ ├── CalculatorSevenButtonLocation.png │ ├── CalculatorSevenButtonNoDpiAdjust.png │ ├── Calculator_icon_225%.PNG │ ├── Chrome_Reload_100%.PNG │ └── MouseMove00WithoutDpi_resized.png ├── Test ├── RunTests.ahk ├── Test_Acc.ahk ├── Test_Array.ahk ├── Test_DPI.ahk ├── Test_Map.ahk ├── Test_Misc.ahk └── Test_String.ahk └── Tools └── WindowSpyDpi.ahk /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "UIA-v2"] 2 | path = UIA-v2 3 | url = https://github.com/Descolada/UIA-v2 4 | [submodule "OCR"] 5 | path = OCR 6 | url = https://github.com/Descolada/OCR 7 | [submodule "Acc-v2"] 8 | path = Acc-v2 9 | url = https://github.com/Descolada/Acc-v2 10 | -------------------------------------------------------------------------------- /Examples/Array_examples.ahk: -------------------------------------------------------------------------------- 1 | #include ..\Lib\Misc.ahk 2 | #include ..\Lib\Array.ahk 3 | 4 | Print := Printer(MsgBox) 5 | Print([4,3,5,1,2].Sort()) 6 | Print(["a", 2, 1.2, 1.22, 1.20].Sort("C")) ; Default is numeric sort, so specify case-sensitivity if the array contains strings. 7 | 8 | myImmovables:=[] 9 | myImmovables.push({town: "New York", size: "60", price: 400000, balcony: 1}) 10 | myImmovables.push({town: "Berlin", size: "45", price: 230000, balcony: 1}) 11 | myImmovables.push({town: "Moscow", size: "80", price: 350000, balcony: 0}) 12 | myImmovables.push({town: "Tokyo", size: "90", price: 600000, balcony: 2}) 13 | myImmovables.push({town: "Palma de Mallorca", size: "250", price: 1100000, balcony: 3}) 14 | Print(myImmovables.Sort("N R", "size")) ; Sort by the key 'size', treating the values as numbers, and sorting in reverse 15 | 16 | Print(["dog", "cat", "mouse"].Map((v) => "a " v)) 17 | 18 | partial := [1,2,3,4,5].Map((a,b) => a+b) 19 | Print("Array: [1,2,3,4,5]`n`nAdd 3 to first element: " partial[1](3) "`nSubtract 1 from third element: " partial[3](-1)) 20 | 21 | Print("Filter integer values from [1,'two','three',4,5]: " ([1,'two','three',4,5].Filter(IsInteger).Join())) 22 | 23 | Print("Sum of elements in [1,2,3,4,5]: " ([1,2,3,4,5].Reduce((a,b) => (a+b)))) 24 | 25 | Print("First value in [1,2,3,4,5] that is an even number: " ([1,2,3,4,5].Find((v) => (Mod(v,2) == 0)))) -------------------------------------------------------------------------------- /Examples/BufferedHotstrings_examples.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #include ..\Lib\BufferedHotstrings.ahk 3 | 4 | ; #MaxThreadsPerHotkey needs to be higher than 1, otherwise some hotstrings might get lost 5 | ; if their activation strings were buffered. 6 | #MaxThreadsPerHotkey 10 7 | ; Enable X (execution), B0 (no backspacing) and O (omit end-character) for all hotstrings, which are necessary for this library to work. 8 | ; Z option resets the hotstring recognizer after each replacement, as AHK does with auto-replace hotstrings 9 | #Hotstring ZXB0O 10 | 11 | ; For demonstration purposes lets use SendEvent as the default hotstring mode. 12 | ; `#Hotstring SE` won't have the desired effect, because we are not using regular hotstrings. 13 | _HS(, "SE") 14 | 15 | ; Regular hotstrings only need to be wrapped with _HS. This uses SendEvent with default delay 0. 16 | ::fwi::_HS("for your information") 17 | ; Regular hotstring arguments can be used; this sets the keydelay to 40ms (this works since we are using SendMode Event) 18 | :K40:afaik::_HS("as far as I know") 19 | ; Other hotstring arguments can be used as well such as Text 20 | :T:omg::_HS("oh my god{enter}") 21 | ; Backspacing can be limited to n backspaces with Bn 22 | :*:because of it's::_HS("s", "B2") 23 | ; ... however that's usually not necessary, because unlike the default implementation, this one backspaces 24 | ; only the non-matching end of the trigger string 25 | :*:thats::_HS("that's") 26 | ; To use regular hotstrings without _HS, reverse the global changes locally (X0 disables execute, B enables backspacing, O0 reverts omitting endchar) 27 | :X0BO0:btw::by the way -------------------------------------------------------------------------------- /Examples/Media/Example1_CurrentSession.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\Media.ahk 3 | 4 | session := Media.GetCurrentSession() 5 | 6 | MsgBox "App with the current session: " session.SourceAppUserModelId 7 | . "`nPlayback state: " Media.PlaybackStatus[session.PlaybackStatus] 8 | . "`nTitle: " session.Title 9 | . "`nArtist: " session.Artist 10 | . "`nPosition: " session.Position "s (duration: " session.EndTime "s)" 11 | 12 | if (MsgBox(session.PlaybackStatus = Media.PlaybackStatus.Playing ? "Pause media?" : "Play media?",, 0x4) = "Yes") { 13 | if session.PlaybackStatus = Media.PlaybackStatus.Playing 14 | session.Pause() 15 | else 16 | session.Play() 17 | } -------------------------------------------------------------------------------- /Examples/Media/Example2_PlaybackChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\Media.ahk 3 | 4 | ; Start/stop the current session media to capture an event 5 | handle := Media.GetCurrentSession().AddPlaybackInfoChangedEvent(PlaybackInfoChangedEventHandler) 6 | Persistent() 7 | 8 | PlaybackInfoChangedEventHandler(session, *) { 9 | MsgBox "Playback info changed!" 10 | . "`nPlaybackStatus: " Media.PlaybackStatus[session.PlaybackStatus] 11 | . "`nPlaybackType: " Media.PlaybackType[session.PlaybackType] 12 | } 13 | 14 | Esc::handle.Remove() -------------------------------------------------------------------------------- /Examples/Media/Example3_GetThumbnail.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\Media.ahk 3 | #include ImagePut.ahk ; Download here: https://github.com/iseahound/ImagePut 4 | 5 | thumbnail := Media.GetCurrentSession().Thumbnail 6 | ImagePutClipboard(thumbnail) ; Copies the thumbnail to the clipboard -------------------------------------------------------------------------------- /Examples/Media/Example4_SessionChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\Media.ahk 3 | 4 | ; Start a new session to capture an event (eg first start-stop Spotify, then Youtube) 5 | handle := Media.AddCurrentSessionChangedEvent(CurrentSessionChangedEventHandler) 6 | Persistent() 7 | 8 | CurrentSessionChangedEventHandler(session, *) { 9 | MsgBox "Session changed!" 10 | . "`nSourceAppUserModelId: " Media.GetCurrentSession().SourceAppUserModelId 11 | } 12 | 13 | Esc::handle.Remove() -------------------------------------------------------------------------------- /Examples/Misc_examples.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\Lib\Misc.ahk 3 | 4 | ; ----------------- Print ---------------------- 5 | Print := Printer(OutputDebug) 6 | Print("Hello") ; Uses OutputDebug to print the string 'Hello' 7 | 8 | Print.OutputFunc := MsgBox ; Next calls of Print will use MsgBox to display the value 9 | Print({example:"Object", key:"value"}) 10 | ; ----------------- Range ---------------------- 11 | 12 | ; Loop forwards, equivalent to Loop 10 13 | result := "" 14 | for v in Range(10) 15 | result .= v "`n" 16 | Print(result) 17 | 18 | ; Loop backwards, equivalent to Range(1,10,-1) 19 | Print(Range(10,1).ToArray()) 20 | 21 | ; Loop forwards, step 2 22 | Print(Range(-10,10,2).ToArray()) 23 | 24 | ; Nested looping 25 | result := "" 26 | for v in Range(3) 27 | for k in Range(5,1) 28 | result .= v " " k "`n" 29 | Print(result) 30 | 31 | ; ----------------- RegExMatchAll ---------------------- 32 | 33 | result := "" 34 | matches := RegExMatchAll("a,bb,ccc", "\w+") 35 | for i, match in matches 36 | result .= "Match " i ": " match[] "`n" 37 | Print(result) -------------------------------------------------------------------------------- /Examples/String_examples.ahk: -------------------------------------------------------------------------------- 1 | #include ..\Lib\String.ahk 2 | 3 | str := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 4 | 5 | MsgBox("First character: " str[1]) 6 | MsgBox("Substring for characters 2-7: " str[2,7]) 7 | MsgBox("Substring starting from character 10: " str[10,0]) 8 | 9 | MsgBox("Reversed: " str.Reverse()) 10 | MsgBox("Word wrapped: `n" str.WordWrap()) 11 | MsgBox("Concatenate: " "; ".Concat("First", "second", 123)) -------------------------------------------------------------------------------- /Examples/WinEvent/Example1_NotepadCreated.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\WinEvent.ahk 3 | 4 | ; Detects when a Notepad window is created. Press F1 to run Notepad and test. 5 | 6 | ; This could also be achieved using WinEvent.Create, but along with the Notepad main window there 7 | ; are some other hidden windows created as well that match "ahk_exe notepad.exe" which we don't want 8 | ; to capture. In the case of Notepad we could use "ahk_class Notepad ahk_exe notepad.exe" to filter 9 | ; for the main window, but that method isn't generalizable, so WinEvent.Show is a safer option. 10 | WinEvent.Show(NotepadCreated, "ahk_exe notepad.exe") 11 | 12 | NotepadCreated(hWnd, hook, dwmsEventTime) { 13 | ToolTip "Notepad was created at " dwmsEventTime ", hWnd " hWnd "`n" 14 | SetTimer ToolTip, -3000 15 | } 16 | 17 | F1::Run("notepad.exe") -------------------------------------------------------------------------------- /Examples/WinEvent/Example2_WindowClose.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\WinEvent.ahk 3 | 4 | Run "notepad.exe" 5 | WinWaitActive "ahk_exe notepad.exe" 6 | ; Detect the closing of the newly created Notepad window. Note that using "A" instead of 7 | ; WinExist("A") would detect the closing of any active window, not Notepad. 8 | WinEvent.Close(ActiveWindowClosed, WinExist("A"), 1) 9 | 10 | ActiveWindowClosed(*) { 11 | MsgBox "Notepad window closed, press OK to exit" 12 | ExitApp 13 | } -------------------------------------------------------------------------------- /Examples/WinEvent/Example3_WindowMaximized.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\WinEvent.ahk 3 | 4 | ; Detects when any window is maximized, for a maximum of 1 successful callback 5 | WinEvent.Maximize(WindowMaximizedEvent,, 1) 6 | 7 | ; Fail the callback if "Yes" is not clicked, so the hook is not stopped 8 | WindowMaximizedEvent(hWnd, hook, dwmsEventTime) => MsgBox("A window was maximized at " dwmsEventTime ", hWnd " hWnd "`n`nStop hook?",, 0x4) != "Yes" 9 | 10 | F1::Run("notepad.exe") -------------------------------------------------------------------------------- /Examples/WinEvent/Example4_ActiveWindowChanged.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\WinEvent.ahk 3 | #include ..\..\Lib\Misc.ahk 4 | 5 | WinEvent.Active(ActiveWindowChanged) 6 | 7 | ActiveWindowChanged(hWnd, *) { 8 | ToolTip "Active window changed! New window info: `n" WinGetInfo(hWnd) 9 | SetTimer ToolTip, -5000 10 | } -------------------------------------------------------------------------------- /Examples/WinEvent/Example5_DockGui.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | #include ..\..\Lib\WinEvent.ahk 3 | 4 | SetWinDelay(-1) 5 | 6 | target := WinExist("ahk_exe notepad.exe") 7 | if !target { 8 | Run "notepad.exe" 9 | WinWait "ahk_exe notepad.exe" 10 | target := WinExist("ahk_exe notepad.exe") 11 | } 12 | WinActivate target 13 | WinWaitActive target 14 | 15 | docked := Gui("-DPIScale +ToolWindow +Owner" target, "Docked GUI") 16 | docked.AddButton(, "Test").OnEvent("Click", (*) => MsgBox("Button was clicked")) 17 | WinGetPosEx(target, &X, &Y, &W, , &offsetX) 18 | WinGetPos(,,, &H, target) 19 | docked.Show("x" (X+W+offsetX) " y" Y " w200 h" H) 20 | docked.Move(X+W+offsetX, Y,, H) 21 | 22 | WinEvent.Move(TargetMoved, target) 23 | WinEvent.Close((*) => ExitApp(), target) 24 | 25 | TargetMoved(hWnd, *) { 26 | local X, Y, W, H 27 | docked.Restore() 28 | WinGetPosEx(target, &X, &Y, &W) 29 | WinGetPos(,,, &H, target) 30 | docked.Move(X+W+offsetX, Y,, H) 31 | } 32 | 33 | ;------------------------------ 34 | ; 35 | ; Function: WinGetPosEx 36 | ; 37 | ; Description: 38 | ; 39 | ; Gets the position, size, and offset of a window. See the *Remarks* section 40 | ; for more information. 41 | ; 42 | ; https://www.autohotkey.com/boards/viewtopic.php?f=6&t=3392 43 | ; 44 | ; Parameters: 45 | ; 46 | ; hWindow - Handle to the window. 47 | ; 48 | ; X, Y, Width, Height - Output variables. [Optional] If defined, these 49 | ; variables contain the coordinates of the window relative to the 50 | ; upper-left corner of the screen (X and Y), and the Width and Height of 51 | ; the window. 52 | ; 53 | ; Offset_X, Offset_Y - Output variables. [Optional] Offset, in pixels, of the 54 | ; actual position of the window versus the position of the window as 55 | ; reported by GetWindowRect. If moving the window to specific 56 | ; coordinates, add these offset values to the appropriate coordinate 57 | ; (X and/or Y) to reflect the true size of the window. 58 | ; 59 | ; Returns: 60 | ; 61 | ; If successful, a RECTPlus buffer object is returned. 62 | ; The first 16 bytes contains a RECT structure that contains the dimensions of the 63 | ; bounding rectangle of the specified window. 64 | ; The dimensions are given in screen coordinates that are relative to the upper-left 65 | ; corner of the screen. 66 | ; The next 8 bytes contain the X and Y offsets (4-byte integer for X and 67 | ; 4-byte integer for Y). 68 | ; 69 | ; Also if successful (and if defined), the output variables (X, Y, Width, 70 | ; Height, Offset_X, and Offset_Y) are updated. See the *Parameters* section 71 | ; for more more information. 72 | ; 73 | ; If not successful, FALSE is returned. 74 | ; 75 | ; Requirement: 76 | ; 77 | ; Windows 2000+ 78 | ; 79 | ; Remarks, Observations, and Changes: 80 | ; 81 | ; * Starting with Windows Vista, Microsoft includes the Desktop Window Manager 82 | ; (DWM) along with Aero-based themes that use DWM. Aero themes provide new 83 | ; features like a translucent glass design with subtle window animations. 84 | ; Unfortunately, the DWM doesn't always conform to the OS rules for size and 85 | ; positioning of windows. If using an Aero theme, many of the windows are 86 | ; actually larger than reported by Windows when using standard commands (Ex: 87 | ; WinGetPos, GetWindowRect, etc.) and because of that, are not positioned 88 | ; correctly when using standard commands (Ex: gui Show, WinMove, etc.). This 89 | ; function was created to 1) identify the true position and size of all 90 | ; windows regardless of the window attributes, desktop theme, or version of 91 | ; Windows and to 2) identify the appropriate offset that is needed to position 92 | ; the window if the window is a different size than reported. 93 | ; 94 | ; * The true size, position, and offset of a window cannot be determined until 95 | ; the window has been rendered. See the example script for an example of how 96 | ; to use this function to position a new window. 97 | ; 98 | ; * 20150906: The "dwmapi\DwmGetWindowAttribute" function can return odd errors 99 | ; if DWM is not enabled. One error I've discovered is a return code of 100 | ; 0x80070006 with a last error code of 6, i.e. ERROR_INVALID_HANDLE or "The 101 | ; handle is invalid." To keep the function operational during this types of 102 | ; conditions, the function has been modified to assume that all unexpected 103 | ; return codes mean that DWM is not available and continue to process without 104 | ; it. When DWM is a possibility (i.e. Vista+), a developer-friendly messsage 105 | ; will be dumped to the debugger when these errors occur. 106 | ; 107 | ; Credit: 108 | ; 109 | ; Idea and some code from *KaFu* (AutoIt forum) 110 | ; 111 | ;------------------------------------------------------------------------------- 112 | WinGetPosEx(hWindow, &X := "", &Y := "", &Width := "", &Height := "", &Offset_X := "", &Offset_Y := "") { 113 | Static S_OK := 0x0, 114 | DWMWA_EXTENDED_FRAME_BOUNDS := 9 115 | ;-- Get the window's dimensions 116 | ; Note: Only the first 16 bytes of the RECTPlus structure are used by the 117 | ; DwmGetWindowAttribute and GetWindowRect functions. 118 | RECTPlus := Buffer(24,0) 119 | Try { 120 | DWMRC := DllCall("dwmapi\DwmGetWindowAttribute", 121 | "Ptr", hWindow, ;-- hwnd 122 | "UInt", DWMWA_EXTENDED_FRAME_BOUNDS, ;-- dwAttribute 123 | "Ptr", RECTPlus, ;-- pvAttribute 124 | "UInt", 16, ;-- cbAttribute 125 | "UInt") 126 | } 127 | Catch { 128 | Return False 129 | } 130 | ;-- Populate the output variables 131 | X := NumGet(RECTPlus, 0, "Int") ; left 132 | Y := NumGet(RECTPlus, 4, "Int") ; top 133 | R := NumGet(RECTPlus, 8, "Int") ; right 134 | B := NumGet(RECTPlus, 12, "Int") ; bottom 135 | Width := R - X ; right - left 136 | Height := B - Y ; bottom - top 137 | OffSet_X := 0 138 | OffSet_Y := 0 139 | ;-- Collect dimensions via GetWindowRect 140 | RECT := Buffer(16, 0) 141 | DllCall("GetWindowRect", "Ptr", hWindow,"Ptr", RECT) 142 | ;-- Right minus Left 143 | GWR_Width := NumGet(RECT, 8, "Int") - NumGet(RECT, 0, "Int") 144 | ;-- Bottom minus Top 145 | GWR_Height:= NumGet(RECT, 12, "Int") - NumGet(RECT, 4, "Int") 146 | ;-- Calculate offsets and update output variables 147 | NumPut("Int", Offset_X := (Width - GWR_Width) // 2, RECTPlus, 16) 148 | NumPut("Int", Offset_Y := (Height - GWR_Height) // 2, RECTPlus, 20) 149 | Return RECTPlus 150 | } -------------------------------------------------------------------------------- /Examples/XHotstring_example.ahk: -------------------------------------------------------------------------------- 1 | #include ..\Lib\XHotstring.ahk 2 | #Requires AutoHotkey v2.0 3 | 4 | ; Can be used as normal hotstrings 5 | XHotstring("::omg", "oh my god") 6 | 7 | ; Replace 'l1' with 'level 1', 'l2' with 'level 2' etc 8 | XHotstring("::l(\d+)", "level $1") 9 | 10 | ; Type Unicode characters with '{U+1234}' (any four hexadecimal numbers) 11 | XHotstring(":O:{U\+([0-9A-F]{4})}", "{U+$1}") 12 | 13 | ; Type 'input: ', then some word/letters/numbers, and press an end character to display the written string 14 | XHotstring("::input: (\S+)", (Match, *) => (ToolTip("You wrote: '" Match[1] "'"), SetTimer(ToolTip, -3000))) 15 | 16 | ; Convert from between kg and lbs, or Celcius and Fahrenheit by typing for example "!conv 120kg to lbs " 17 | XHotstring("::!conv (?[\d.]+)(?\s*)(?\S+) to (?\S+)", ConvertUnit) 18 | 19 | ; Next hotstrings only work in Chrome-based windows 20 | XHotstring.HotIf((*) => WinActive("ahk_class Chrome_WidgetWin_1")) 21 | ; Replace '@gc' with '@gmail.com' only if it is preceded by alphanumeric characters and/or periods 22 | XHotstring(":*?:[\w.]+\K@gc", "@gmail.com") 23 | 24 | ; Stop or resume the hotstring recognizer by pressing End key 25 | End::(XHotstring.IsActive) ? XHotstring.Stop() : XHotstring.Start() 26 | 27 | ConvertUnit(Match, EndChar, *) { 28 | switch Match["Unit"], 0 { 29 | case "kg": 30 | if Match["TargetUnit"] = "lbs" || Match["TargetUnit"] = "lb" 31 | Converted := Round(Float(Match["Number"]) * 2.20462) 32 | case "lbs", "lb": 33 | if Match["TargetUnit"] = "kg" 34 | Converted := Round(Float(Match["Number"]) / 2.20462) 35 | case "C", "°C": 36 | if InStr(Match["TargetUnit"], "F") 37 | Converted := Round((Float(Match["Number"]) * 9 / 5) + 32) 38 | case "F", "°F": 39 | if InStr(Match["TargetUnit"], "C") 40 | Converted := Round((Float(Match["Number"]) - 32) * 5 / 9) 41 | } 42 | if IsSet(Converted) 43 | Send Converted Match["Space"] Match["TargetUnit"] EndChar 44 | else 45 | MsgBox "No conversion between " Match["Unit"] " and " Match["TargetUnit"] " possible" 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Descolada 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. -------------------------------------------------------------------------------- /Lib/Array.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | Name: Array.ahk 3 | Version 0.4.1 (02.09.24) 4 | Created: 27.08.22 5 | Author: Descolada 6 | 7 | Description: 8 | A compilation of useful array methods. 9 | 10 | Array.Slice(start:=1, end:=0, step:=1) => Returns a section of the array from 'start' to 'end', 11 | optionally skipping elements with 'step'. 12 | Array.Swap(a, b) => Swaps elements at indexes a and b. 13 | Array.Map(func, arrays*) => Applies a function to each element in the array and returns a new array. 14 | Array.ForEach(func) => Calls a function for each element in the array. 15 | Array.Filter(func) => Keeps only values that satisfy the provided function and returns a new array with the results. 16 | Array.Reduce(func, initialValue?) => Applies a function cumulatively to all the values in 17 | the array, with an optional initial value. 18 | Array.IndexOf(value, start:=1) => Finds a value in the array and returns its index. 19 | Array.Find(func, &match?, start:=1) => Finds a value satisfying the provided function and returns the index. 20 | match will be set to the found value. 21 | Array.Reverse() => Reverses the array. 22 | Array.Count(value) => Counts the number of occurrences of a value. 23 | Array.Sort(OptionsOrCallback?, Key?) => Sorts an array, optionally by object values. 24 | Array.Shuffle() => Randomizes the array. 25 | Array.Unique() => Returns a set of the values in the array 26 | Array.Join(delim:=",") => Joins all the elements to a string using the provided delimiter. 27 | Array.Flat() => Turns a nested array into a one-level array. 28 | Array.Extend(enums*) => Adds the values of other arrays or enumerables to the end of this one. 29 | */ 30 | 31 | class Array2 { 32 | static __New() => (Array2.base := Array.Prototype.base, Array.Prototype.base := Array2) 33 | /** 34 | * Returns a section of the array from 'start' to 'end', optionally skipping elements with 'step'. 35 | * Modifies the original array. 36 | * @param start Optional: index to start from. Default is 1. 37 | * @param end Optional: index to end at. Can be negative. Default is 0 (includes the last element). 38 | * @param step Optional: an integer specifying the incrementation. Default is 1. 39 | * @returns {Array} 40 | */ 41 | static Slice(start:=1, end:=0, step:=1) { 42 | len := this.Length, i := start < 1 ? len + start + 1 : start, j := Min(end < 1 ? len + end + 1 : end, len), r := [], reverse := False 43 | if step = 0 44 | Throw ValueError("Slice: step cannot be 0", -1) 45 | if len = 0 46 | return [] 47 | if i < 1 48 | i := 1 49 | r.Length := r.Capacity := Abs(j+1-i) // step 50 | if step < 0 { 51 | while i >= j { 52 | if this.Has(i) 53 | r[A_Index] := this[i] 54 | i += step 55 | } 56 | } else { 57 | while i <= j { 58 | if this.Has(i) 59 | r[A_Index] := this[i] 60 | i += step 61 | } 62 | } 63 | return r 64 | } 65 | /** 66 | * Swaps elements at indexes a and b 67 | * @param a First elements index to swap 68 | * @param b Second elements index to swap 69 | * @returns {Array} 70 | */ 71 | static Swap(a, b) { 72 | temp := this.Has(b) ? this[b] : unset 73 | this.Has(a) ? (this[b] := this[a]) : this.Delete(b) 74 | IsSet(temp) ? (this[a] := temp) : this.Delete(a) 75 | return this 76 | } 77 | /** 78 | * Applies a function to each element in the array and returns a new array with the results. 79 | * @param func The mapping function that accepts one argument. 80 | * @param arrays Additional arrays to be accepted in the mapping function 81 | * @returns {Array} 82 | */ 83 | static Map(func, arrays*) { 84 | if !HasMethod(func) 85 | throw ValueError("Map: func must be a function", -1) 86 | local new := this.Clone() 87 | for i, v in new { 88 | bf := func.Bind(v?) 89 | for _, vv in arrays 90 | bf := bf.Bind(vv.Has(i) ? vv[i] : unset) 91 | try bf := bf() 92 | new[i] := bf 93 | } 94 | return new 95 | } 96 | /** 97 | * Applies a function to each element in the array. 98 | * @param func The callback function with arguments Callback(value[, index, array]). 99 | * @returns {Array} 100 | */ 101 | static ForEach(func) { 102 | if !HasMethod(func) 103 | throw ValueError("ForEach: func must be a function", -1) 104 | for i, v in this 105 | func(v?, i, this) 106 | return this 107 | } 108 | /** 109 | * Keeps only values that satisfy the provided function and returns a new array with the results. 110 | * @param func The filter function that accepts one argument. 111 | * @returns {Array} 112 | */ 113 | static Filter(func) { 114 | if !HasMethod(func) 115 | throw ValueError("Filter: func must be a function", -1) 116 | r := [] 117 | for v in this 118 | if func(v?) 119 | r.Push(v) 120 | return r 121 | } 122 | /** 123 | * Applies a function cumulatively to all the values in the array, with an optional initial value. 124 | * @param func The function that accepts two arguments and returns one value 125 | * @param initialValue Optional: the starting value. If omitted, the first value in the array is used. 126 | * @returns {func return type} 127 | * @example 128 | * [1,2,3,4,5].Reduce((a,b) => (a+b)) ; returns 15 (the sum of all the numbers) 129 | */ 130 | static Reduce(func, initialValue?) { 131 | if !HasMethod(func) 132 | throw ValueError("Reduce: func must be a function", -1) 133 | len := this.Length + 1 134 | if len = 1 135 | return initialValue ?? "" 136 | if IsSet(initialValue) 137 | out := initialValue, i := 0 138 | else 139 | out := this[1], i := 1 140 | while ++i < len { 141 | out := func(out?, this.Has(i) ? this[i] : unset) 142 | } 143 | return out 144 | } 145 | /** 146 | * Finds a value in the array and returns its index. 147 | * @param value The value to search for. 148 | * @param start Optional: the index to start the search from, negative start reverses the search. Default is 1. 149 | */ 150 | static IndexOf(value?, start:=1) { 151 | local len := this.Length, reverse := false 152 | if !IsInteger(start) 153 | throw ValueError("IndexOf: start value must be an integer", -1) 154 | if start < 0 155 | reverse := true, start := len+1+start 156 | if start < 1 || start > len 157 | return 0 158 | if reverse { 159 | ++start 160 | if IsSet(value) { 161 | while --start > 0 162 | if this.Has(start) && (this[start] == value) 163 | return start 164 | } else { 165 | while --start > 0 166 | if !this.Has(start) 167 | return start 168 | } 169 | } else { 170 | --start 171 | if IsSet(value) { 172 | while ++start <= len 173 | if this.Has(start) && (this[start] == value) 174 | return start 175 | } else { 176 | while ++start <= len 177 | if !this.Has(start) 178 | return start 179 | } 180 | } 181 | return 0 182 | } 183 | /** 184 | * Finds a value satisfying the provided function and returns its index. 185 | * @param func The condition function that accepts one argument. 186 | * @param match Optional: is set to the found value 187 | * @param start Optional: the index to start the search from, negative start reverses the search. Default is 1. 188 | * @example 189 | * [1,2,3,4,5].Find((v) => (Mod(v,2) == 0)) ; returns 2 190 | */ 191 | static Find(func, &match?, start:=1) { 192 | local reverse := false 193 | if !HasMethod(func) 194 | throw ValueError("Find: func must be a function", -1) 195 | local len := this.Length 196 | if start < 0 197 | reverse := true, start := len+1+start 198 | if start < 1 || start > len 199 | return 0 200 | 201 | if reverse { 202 | ++start 203 | while --start > 0 204 | if ((v := (this.Has(start) ? this[start] : unset)), func(v?)) 205 | return ((match := v ?? unset), start) 206 | } else { 207 | --start 208 | while ++start <= len 209 | if ((v := (this.Has(start) ? this[start] : unset)), func(v?)) 210 | return ((match := v ?? unset), start) 211 | } 212 | return 0 213 | } 214 | /** 215 | * Reverses the array. 216 | * @example 217 | * [1,2,3].Reverse() ; returns [3,2,1] 218 | */ 219 | static Reverse() { 220 | local len := this.Length + 1, max := (len // 2), i := 0 221 | while ++i <= max 222 | this.Swap(i, len - i) 223 | return this 224 | } 225 | /** 226 | * Counts the number of occurrences of a value 227 | * @param value The value to count. Can also be a function. 228 | */ 229 | static Count(value?) { 230 | count := 0 231 | if !IsSet(value) { 232 | Loop this.Length 233 | if this.Has(A_Index) 234 | count++ 235 | } else if HasMethod(value) { 236 | for _, v in this 237 | if value(v?) 238 | count++ 239 | } else 240 | for _, v in this 241 | if v == value 242 | count++ 243 | return count 244 | } 245 | /** 246 | * Sorts an array, optionally by object keys 247 | * @param OptionsOrCallback Optional: either a callback function, or one of the following: 248 | * 249 | * N => array is considered to consist of only numeric values. This is the default option. 250 | * C, C1 or COn => case-sensitive sort of strings 251 | * C0 or COff => case-insensitive sort of strings 252 | * 253 | * The callback function should accept two parameters elem1 and elem2 and return an integer: 254 | * Return integer < 0 if elem1 less than elem2 255 | * Return 0 is elem1 is equal to elem2 256 | * Return > 0 if elem1 greater than elem2 257 | * @param Key Optional: Omit it if you want to sort a array of primitive values (strings, numbers etc). 258 | * If you have an array of objects, specify here the key by which contents the object will be sorted. 259 | * @returns {Array} 260 | */ 261 | static Sort(optionsOrCallback:="N", key?) { 262 | static sizeofFieldType := 16 ; Same on both 32-bit and 64-bit 263 | if HasMethod(optionsOrCallback) 264 | pCallback := CallbackCreate(CustomCompare.Bind(optionsOrCallback), "F Cdecl", 2), optionsOrCallback := "" 265 | else { 266 | if InStr(optionsOrCallback, "N") 267 | pCallback := CallbackCreate(IsSet(key) ? NumericCompareKey.Bind(key) : NumericCompare, "F CDecl", 2) 268 | if RegExMatch(optionsOrCallback, "i)C(?!0)|C1|COn") 269 | pCallback := CallbackCreate(IsSet(key) ? StringCompareKey.Bind(key,,True) : StringCompare.Bind(,,True), "F CDecl", 2) 270 | if RegExMatch(optionsOrCallback, "i)C0|COff") 271 | pCallback := CallbackCreate(IsSet(key) ? StringCompareKey.Bind(key) : StringCompare, "F CDecl", 2) 272 | if InStr(optionsOrCallback, "Random") 273 | pCallback := CallbackCreate(RandomCompare, "F CDecl", 2) 274 | if !IsSet(pCallback) 275 | throw ValueError("No valid options provided!", -1) 276 | } 277 | mFields := NumGet(ObjPtr(this) + (8 + (VerCompare(A_AhkVersion, "<2.1-") > 0 ? 3 : 5)*A_PtrSize), "Ptr") ; in v2.0: 0 is VTable. 2 is mBase, 3 is mFields, 4 is FlatVector, 5 is mLength and 6 is mCapacity 278 | DllCall("msvcrt.dll\qsort", "Ptr", mFields, "UInt", this.Length, "UInt", sizeofFieldType, "Ptr", pCallback, "Cdecl") 279 | CallbackFree(pCallback) 280 | if RegExMatch(optionsOrCallback, "i)R(?!a)") 281 | this.Reverse() 282 | if InStr(optionsOrCallback, "U") 283 | this := this.Unique() 284 | return this 285 | 286 | CustomCompare(compareFunc, pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), compareFunc(fieldValue1, fieldValue2)) 287 | NumericCompare(pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), (fieldValue1 > fieldValue2) - (fieldValue1 < fieldValue2)) 288 | NumericCompareKey(key, pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), (f1 := fieldValue1.HasProp("__Item") ? fieldValue1[key] : fieldValue1.%key%), (f2 := fieldValue2.HasProp("__Item") ? fieldValue2[key] : fieldValue2.%key%), (f1 > f2) - (f1 < f2)) 289 | StringCompare(pFieldType1, pFieldType2, casesense := False) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), StrCompare(fieldValue1 "", fieldValue2 "", casesense)) 290 | StringCompareKey(key, pFieldType1, pFieldType2, casesense := False) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), StrCompare(fieldValue1.%key% "", fieldValue2.%key% "", casesense)) 291 | RandomCompare(pFieldType1, pFieldType2) => (Random(0, 1) ? 1 : -1) 292 | 293 | ValueFromFieldType(pFieldType, &fieldValue?) { 294 | static SYM_STRING := 0, PURE_INTEGER := 1, PURE_FLOAT := 2, SYM_MISSING := 3, SYM_OBJECT := 5 295 | switch SymbolType := NumGet(pFieldType + 8, "Int") { 296 | case PURE_INTEGER: fieldValue := NumGet(pFieldType, "Int64") 297 | case PURE_FLOAT: fieldValue := NumGet(pFieldType, "Double") 298 | case SYM_STRING: fieldValue := StrGet(NumGet(pFieldType, "Ptr")+2*A_PtrSize) 299 | case SYM_OBJECT: fieldValue := ObjFromPtrAddRef(NumGet(pFieldType, "Ptr")) 300 | case SYM_MISSING: return 301 | } 302 | } 303 | } 304 | /** 305 | * Randomizes the array. Slightly faster than Array.Sort(,"Random N") 306 | * @returns {Array} 307 | */ 308 | static Shuffle() { 309 | len := this.Length 310 | Loop len-1 311 | this.Swap(A_index, Random(A_index, len)) 312 | return this 313 | } 314 | /** 315 | * Returns a set of values in array 316 | */ 317 | static Unique() { 318 | unique := Map() 319 | for v in this 320 | unique[v] := 1 321 | return [unique*] 322 | } 323 | /** 324 | * Joins all the elements to a string using the provided delimiter. 325 | * @param delim Optional: the delimiter to use. Default is comma. 326 | * @returns {String} 327 | */ 328 | static Join(delim:=",") { 329 | result := "" 330 | for v in this 331 | result .= (v ?? "") delim 332 | return (len := StrLen(delim)) ? SubStr(result, 1, -len) : result 333 | } 334 | /** 335 | * Turns a nested array into a one-level array 336 | * @returns {Array} 337 | * @example 338 | * [1,[2,[3]]].Flat() ; returns [1,2,3] 339 | */ 340 | static Flat() { 341 | r := [] 342 | for v in this { 343 | if !IsSet(v) 344 | r.Length += 1 345 | else if v is Array 346 | r.Push(v.Flat()*) 347 | else 348 | r.Push(v) 349 | } 350 | return this := r 351 | } 352 | /** 353 | * Adds the contents of another array to the end of this one. 354 | * @param enums The arrays or other enumerables that are used to extend this one. 355 | * @returns {Array} 356 | */ 357 | static Extend(enums*) { 358 | for enum in enums { 359 | if !HasMethod(enum, "__Enum") 360 | throw ValueError("Extend: arr must be an iterable") 361 | for _, v in enum 362 | this.Push(v) 363 | } 364 | return this 365 | } 366 | } -------------------------------------------------------------------------------- /Lib/BufferedHotstrings.ahk: -------------------------------------------------------------------------------- 1 | #requires AutoHotkey v2.0.14 2 | /** 3 | * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't 4 | * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no 5 | * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0` 6 | * Note that mouse clicks *will* interrupt sending keystrokes. 7 | * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options 8 | * will be modified according to the provided opts. 9 | * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 10 | * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 11 | * NOT be used, correct is `_HS("hotstring", "B0")`). 12 | * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing 13 | * deletes only the non-matching end of the trigger string (compared to the replacement string). 14 | * Use the `BF` option to delete the whole trigger string. 15 | * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 16 | * of the trigger string. 17 | * 18 | * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring` 19 | * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring 20 | * local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect. 21 | * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`. 22 | * 23 | * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided. 24 | * SendMode can only be changed with this (`#Hotstring SE` has no effect). 25 | * @param sendFunc Optional: this can be used to define a default custom send function (if replacement 26 | * is left empty), or temporarily use a custom function. This could, for example, be used to send 27 | * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 28 | * ending character is still done with the normal Send function. 29 | * @returns {void} 30 | */ 31 | _HS(replacement?, opts?, sendFunc?) { 32 | static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0 33 | , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true 34 | , __Init := HotstringRecognizer.Start() 35 | ; Save global variables ASAP to avoid these being modified if _HS is interrupted 36 | local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode 37 | , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1) 38 | , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar)) 39 | 40 | ; Only options without replacement text changes the global/default options 41 | if !IsSet(replacement) { 42 | if IsSet(sendFunc) 43 | DefaultCustomSendFunc := sendFunc 44 | if IsSet(opts) { 45 | i := 1, opts := StrReplace(opts, " "), len := StrLen(opts) 46 | While i <= len { 47 | o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1) 48 | if o = "S" { 49 | ; SendMode is reset if no SendMode is specifically provided 50 | DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input") 51 | i += 2 52 | continue 53 | } else if o = "O" 54 | DefaultOmit := o_next != "0" 55 | else if o = "*" 56 | DefaultOmit := o_next != "0" 57 | else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) { 58 | i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0]) 59 | continue 60 | } else if o = "T" 61 | DefaultTextMode := o_next = "0" ? "" : "{Text}" 62 | else if o = "R" 63 | DefaultTextMode := o_next = "0" ? "" : "{Raw}" 64 | else if o = "B" { 65 | ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0 66 | continue 67 | } else if o = "C" 68 | DefaultCaseConform := o_next = "0" ? 1 : 0 69 | i += IsNumber(o_next) ? 2 : 1 70 | } 71 | } 72 | return 73 | } 74 | if !IsSet(replacement) 75 | return 76 | ; Musn't use Critical here, otherwise InputBuffer callbacks won't work 77 | ; Start capturing input for the rare case where keys are sent during options parsing 78 | HSInputBuffer.Start() 79 | 80 | TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform 81 | SendMode DefaultSendMode 82 | if InStr(DefaultSendMode, "Play") 83 | SetKeyDelay , DefaultKeyDelay, "Play" 84 | else 85 | SetKeyDelay DefaultKeyDelay 86 | 87 | ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 88 | ; be changed with local hotstring options 89 | if IsSet(opts) && InStr(opts, "B") 90 | BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0 91 | ; Load local hotstring options, but don't check for backspacing 92 | if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 93 | opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts) 94 | While i <= len { 95 | o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1) 96 | if o = "S" { 97 | SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input") 98 | i += 2 99 | continue 100 | } else if o = "O" 101 | Omit := o_next != "0" 102 | else if o = "*" 103 | Omit := o_next != "0" 104 | else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) { 105 | i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0]) 106 | if InStr(A_SendMode, "Play") 107 | SetKeyDelay , KeyDelay, "Play" 108 | else 109 | SetKeyDelay KeyDelay 110 | continue 111 | } else if o = "T" 112 | TextMode := o_next = "0" ? "" : "{Text}" 113 | else if o = "R" 114 | TextMode := o_next = "0" ? "" : "{Raw}" 115 | else if o = "C" 116 | CaseConform := o_next = "0" ? 1 : 0 117 | i += IsNumber(o_next) ? 2 : 1 118 | } 119 | } 120 | 121 | if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale') { 122 | if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale') 123 | replacement := StrUpper(replacement), Trigger := StrUpper(Trigger) 124 | else 125 | replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2) 126 | } 127 | 128 | ; If backspacing is enabled, get the activation string length using Unicode character length 129 | ; since graphemes need one backspace to be deleted but regular StrLen would report more than one 130 | if BS { 131 | RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_", &MaxBS:=0) 132 | if BS = 0xFFFFFFF0 { 133 | BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1}) 134 | RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)") 135 | if !TextMode && info.GraphemeLength && (SpecialChar := RegExMatch(Trigger, "[\Q^+!#{}\E]")) && SpecialChar < info.Pos { 136 | RegExReplace(SubStr(Trigger, 1, SpecialChar), "s)\X",, &Diff:=0) 137 | BS := MaxBS - Diff + 1, replacement := SubStr(replacement, SpecialChar) 138 | } else { 139 | BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos) 140 | } 141 | } else 142 | BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS 143 | } 144 | ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed 145 | ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer 146 | if TextMode || !CustomSendFunc 147 | Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar))) 148 | else { 149 | Send((BS ? "{BS " BS "}" : "")) 150 | CustomSendFunc(replacement) 151 | if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs 152 | Send("{Raw}" EndChar) 153 | } 154 | ; Reset the recognizer, so the next step will be captured by it 155 | HotstringRecognizer.Reset() 156 | ; Release the buffer, but restore Send settings *after* it (since it also uses Send) 157 | HSInputBuffer.Stop() 158 | if InStr(A_SendMode, "Play") 159 | SetKeyDelay , PrevKeyDurationPlay, "Play" 160 | else 161 | SetKeyDelay PrevKeyDelay 162 | SendMode PrevSendMode 163 | 164 | GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1 165 | } 166 | 167 | /** 168 | * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically 169 | * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 170 | * 171 | * Properties: 172 | * HotstringRecognizer.Content => the current content of the recognizer 173 | * HotstringRecognizer.Length => length of the content string 174 | * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not 175 | * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured 176 | * HotstringRecognizer.ResetKeys => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior) 177 | * HotstringRecognizer.OnChange => can be set to a callback function that is called when the recognizer content changes. 178 | * The callback receives two arguments: Callback(OldContent, NewContent) 179 | * 180 | * Methods: 181 | * HotstringRecognizer.Start() => starts capturing hotstring content 182 | * HotstringRecognizer.Stop() => stops capturing 183 | * HotstringRecognizer.Reset() => clears the content and resets the internal foreground window 184 | * 185 | */ 186 | class HotstringRecognizer { 187 | static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}" 188 | , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0 189 | 190 | static __New() { 191 | this.__Hook := InputHook("V L0 I" A_SendLevel) 192 | this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N") 193 | this.__Hook.OnKeyDown := this.Reset.Bind(this) 194 | this.__Hook.OnChar := this.__AddChar.Bind(this) 195 | Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)}) 196 | ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode 197 | HotstringRecognizer.DefineProp("MinSendLevel", { 198 | set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 199 | get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)}) 200 | HotstringRecognizer.DefineProp("ResetKeys", 201 | {set:((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 202 | get:((this, *) => this.__ResetKeys).Bind(this)}) 203 | } 204 | 205 | static Start() { 206 | this.Reset() 207 | Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks 208 | this.__Hook.Start() 209 | this.IsActive := 1 210 | } 211 | static Stop() => (this.__Hook.Stop(), this.IsActive := 0, this.__SetMouseReset(0)) 212 | static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.__SetContent(SubStr(this.Content, 1, -1)) : this.__SetContent(""), this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr")) 213 | 214 | static __AddChar(ih, char) { 215 | hWnd := DllCall("GetForegroundWindow", "ptr") 216 | if this.__hWnd != hWnd 217 | this.__hWnd := hwnd, this.__SetContent("") 218 | this.__SetContent(this.Content char), this.Length += 1 219 | if this.Length > 100 220 | this.Length := 50, this.Content := SubStr(this.Content, 52) 221 | } 222 | static __MouseReset(*) { 223 | if Hotstring("MouseReset") 224 | this.Reset() 225 | } 226 | static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) { 227 | switch arg1, 0 { 228 | case "MouseReset": 229 | if IsSet(arg2) 230 | this.__SetMouseReset(arg2) 231 | case "Reset": 232 | this.Reset() 233 | } 234 | return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*) 235 | } 236 | static __SetMouseReset(newValue) { 237 | static MouseRIProc := this.__MouseRawInputProc.Bind(this), DevSize := 8 + A_PtrSize, RIDEV_INPUTSINK := 0x00000100 238 | , RIDEV_REMOVE := 0x00000001, RAWINPUTDEVICE := Buffer(DevSize, 0), Active := 0 239 | if !!newValue = Active 240 | return 241 | if Active := !!newValue { 242 | NumPut("UShort", 1, "UShort", 2, "Uint", RIDEV_INPUTSINK, "Ptr", A_ScriptHwnd, RAWINPUTDEVICE) 243 | DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize) 244 | OnMessage(0x00FF, MouseRIProc) 245 | } else { 246 | OnMessage(0x00FF, MouseRIProc, 0) 247 | NumPut("Uint", RIDEV_REMOVE, RAWINPUTDEVICE, 4) 248 | DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize) 249 | } 250 | } 251 | static __MouseRawInputProc(wParam, lParam, *) { 252 | ; RawInput statics 253 | static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, pcbSize:=8+2*A_PtrSize, offsets := {usButtonFlags: (12+2*A_PtrSize), x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput 254 | ; Get hDevice from RAWINPUTHEADER to identify which mouse this data came from 255 | header := Buffer(pcbSize, 0) 256 | If !DllCall("GetRawInputData", "Ptr", lParam, "uint", 0x10000005, "Ptr", header, "Uint*", &pcbSize, "Uint", pcbSize) 257 | return 0 258 | ThisMouse := NumGet(header, 8, "UPtr") 259 | ; Find size of rawinput data - only needs to be run the first time. 260 | if (!iSize) { 261 | r := DllCall("GetRawInputData", "Ptr", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", &iSize, "UInt", 8 + (A_PtrSize * 2)) 262 | uRawInput := Buffer(iSize, 0) 263 | } 264 | ; Get RawInput data 265 | r := DllCall("GetRawInputData", "Ptr", lParam, "UInt", 0x10000003, "Ptr", uRawInput, "UInt*", &sz := iSize, "UInt", 8 + (A_PtrSize * 2)) 266 | 267 | usButtonFlags := NumGet(uRawInput, offsets.usButtonFlags, "ushort") 268 | 269 | if usButtonFlags & 0x0001 || usButtonFlags & 0x0004 || usButtonFlags & 0x0010 || usButtonFlags & 0x0040 || usButtonFlags & 0x0100 270 | this.__MouseReset() 271 | } 272 | static __SetContent(Value) { 273 | if this.OnChange && HasMethod(this.OnChange) && this.Content !== Value 274 | SetTimer(this.OnChange.Bind(this.Content, Value), -1) 275 | this.Content := Value 276 | } 277 | } 278 | 279 | /** 280 | * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 281 | * The default InputBuffer (via the main class name) is keyboard only, but new instances 282 | * can be created via InputBuffer(). 283 | * 284 | * InputBuffer(keybd := true, mouse := false, timeout := 0) 285 | * Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 286 | * InputHook settings are used, and if they are a string then they are used as the Option 287 | * arguments for InputHook and HotKey functions. Timeout can optionally be provided to call 288 | * InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe). 289 | * 290 | * InputBuffer.Start() => initiates capturing input 291 | * InputBuffer.Release() => releases buffered input and continues capturing input 292 | * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input 293 | * InputBuffer.ActiveCount => current number of Start() calls 294 | * Capturing will stop only when this falls to 0 (Stop() decrements it by 1) 295 | * InputBuffer.SendLevel => SendLevel of the InputHook 296 | * InputBuffers default capturing SendLevel is A_SendLevel+2, 297 | * and key release SendLevel is A_SendLevel+1. 298 | * InputBuffer.IsReleasing => whether Release() is currently in action 299 | * InputBuffer.Buffer => current buffered input in an array 300 | * 301 | * Notes: 302 | * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send) 303 | */ 304 | class InputBuffer { 305 | Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map() 306 | , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"] 307 | , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"] 308 | static __New() => this.DefineProp("Default", {value:InputBuffer()}) 309 | static __Get(Name, Params) => this.Default.%Name% 310 | static __Set(Name, Params, Value) => this.Default.%Name% := Value 311 | static __Call(Name, Params) => this.Default.%Name%(Params*) 312 | __New(keybd := true, mouse := false, timeout := 0) { 313 | if !keybd && !mouse 314 | throw Error("At least one input type must be specified") 315 | this.Timeout := timeout 316 | this.Keybd := keybd, this.Mouse := mouse 317 | if keybd { 318 | if keybd is String { 319 | if RegExMatch(keybd, "i)I *(\d+)", &lvl) 320 | this.SendLevel := Integer(lvl[1]) 321 | } 322 | this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0") 323 | this.InputHook.NotifyNonText := true 324 | this.InputHook.VisibleNonText := false 325 | this.InputHook.OnKeyDown := this.BufferKey.Bind(this,,,, "Down") 326 | this.InputHook.OnKeyUp := this.BufferKey.Bind(this,,,, "Up") 327 | this.InputHook.KeyOpt("{All}", "N S") 328 | } 329 | this.HotIfIsActive := this.GetActiveCount.Bind(this) 330 | } 331 | BufferMouse(ThisHotkey, Opts := "") { 332 | savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen") 333 | MouseGetPos(&X, &Y) 334 | ThisHotkey := StrReplace(ThisHotkey, "Button") 335 | this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts)) 336 | CoordMode("Mouse", savedCoordMode) 337 | } 338 | BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD))) 339 | Start() { 340 | this.ActiveCount += 1 341 | SetTimer(this.Stop.Bind(this), -this.Timeout) 342 | 343 | if this.ActiveCount > 1 344 | return 345 | 346 | this.Buffer := [], this.ModifierKeyStates := Map() 347 | for modifier in this.ModifierKeys 348 | this.ModifierKeyStates[modifier] := GetKeyState(modifier) 349 | 350 | if this.Keybd 351 | this.InputHook.Start() 352 | if this.Mouse { 353 | HotIf this.HotIfIsActive 354 | if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl) 355 | this.SendLevel := Integer(lvl[1]) 356 | opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel) 357 | for key in this.MouseButtons { 358 | if InStr(key, "Wheel") 359 | HotKey key, this.BufferMouse.Bind(this), opts 360 | else { 361 | HotKey key, this.BufferMouse.Bind(this,, "Down"), opts 362 | HotKey key " Up", this.BufferMouse.Bind(this), opts 363 | } 364 | } 365 | HotIf ; Disable context sensitivity 366 | } 367 | } 368 | Release() { 369 | if this.IsReleasing || !this.Buffer.Length 370 | return [] 371 | 372 | sent := [], clickSent := false, this.IsReleasing := 1 373 | if this.Mouse 374 | savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y) 375 | 376 | ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case 377 | ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 378 | ; were added to the buffer and send those as well until the buffer is emptied. 379 | PrevSendLevel := A_SendLevel 380 | SendLevel this.SendLevel - 1 381 | 382 | ; Restore the state of any modifier keys before input buffering was started 383 | modifierList := "" 384 | for modifier, state in this.ModifierKeyStates 385 | if GetKeyState(modifier) != state 386 | modifierList .= "{" modifier (state ? " Down" : " Up") "}" 387 | if modifierList 388 | Send modifierList 389 | 390 | while this.Buffer.Length { 391 | key := this.Buffer.RemoveAt(1) 392 | sent.Push(key) 393 | if InStr(key, "{Click ") 394 | clickSent := true 395 | Send("{Blind}" key) 396 | } 397 | SendLevel PrevSendLevel 398 | 399 | if this.Mouse && clickSent { 400 | MouseMove(X, Y) 401 | CoordMode("Mouse", savedCoordMode) 402 | } 403 | this.IsReleasing := 0 404 | return sent 405 | } 406 | Stop(release := true) { 407 | if !this.ActiveCount 408 | return 409 | 410 | sent := release ? this.Release() : [] 411 | 412 | if --this.ActiveCount 413 | return 414 | 415 | if this.Keybd 416 | this.InputHook.Stop() 417 | 418 | if this.Mouse { 419 | HotIf this.HotIfIsActive 420 | for key in this.MouseButtons 421 | HotKey key, "Off" 422 | HotIf ; Disable context sensitivity 423 | } 424 | 425 | return sent 426 | } 427 | GetActiveCount(HotkeyName) => this.ActiveCount 428 | } -------------------------------------------------------------------------------- /Lib/DPI.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | 3 | /* 4 | Name: DPI.ahk 5 | Version 1.0 (07.09.23) 6 | Created: 01.09.23 7 | Author: Descolada 8 | 9 | Description: 10 | A library meant to standardize coordinates between computers/monitors with different DPIs, including multi-monitor setups. 11 | 12 | How this works: 13 | This library, by default, normalizes all output coordinates to 96 DPI (100% scale) and all input coordinates to the DPI of the target window (if CoordMode is Client or Window). 14 | Accompanied is WindowSpyDpi.ahk which is WindowSpy modified to normalize all coordinates to DPI 96. This can be used to get coordinates for your script. 15 | Then use the corresponding DPI.Win... (eg DPI.WinGetPos) variant of the function you wish to use, but using the normalized coordinates. 16 | If screen coordinates are used (CoordMode Screen) then usually they don't need to be converted and native functions can be used. 17 | In addition, when DPI.ahk is used then the DPI awareness of the script is automatically set to monitor-aware. This might break compatibility with existing scripts. 18 | 19 | For example, using the default CoordMode("Mouse", "Client"), DPI.MouseGetPos(&outX, &outY) will return coordinates scaled to DPI 96 taking account the monitor and window DPIs. 20 | Then, DPI.MouseMove(outX, outY) will convert the coordinates back to the proper DPI. This means that the coordinates from DPI.MouseGetPos can be used the same in all computers, all 21 | monitors, and all multi-monitor setups. 22 | 23 | 24 | DPI.ahk constants: 25 | DPI.Standard := 96 means than by default the conversion is to DPI 96, but this global variable can be changed to a higher value (eg 960 for 1000% scaling). 26 | This may be desired if pixel-perfect accuracy is needed. 27 | DPI.MaximumPerMonitorDpiAwarenessContext contains either -3 or -4 depending on Windows version 28 | DPI.DefaultDpiAwarenessContext determines the default DPI awareness which will be set after each Dpi function call, by default it's DPI.MaximumPerMonitorDpiAwarenessContext 29 | 30 | 31 | DPI.ahk functions: 32 | DPI.SetThreadAwarenessContext(context) => Sets DPI awareness of the running script thread to a new context 33 | DPI.SetProcessAwarenessContext(context) => Sets DPI awareness of the running process to a new context 34 | DPI.GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) => Gets the DPI for the specified window 35 | DPI.ConvertCoord(&coord, from, to) => Converts a coordinate from DPI from to DPI to 36 | DPI.FromStandard(dpi, &x, &y) => Converts a point (x,y) from DPI DPI.Standard to the new DPI 37 | DPI.ToStandard(dpi, &x, &y) => Converts a point (x,y) from dpi to DPI.Standard 38 | DPI.GetMonitorHandles() => Returns an array of monitor handles for all active monitors 39 | DPI.MonitorFromPoint(x, y, CoordMode, flags:=2) => Gets monitor from a point (if CoordMode is not "screen" then also adjusts for DPI) 40 | DPI.MonitorFromWindow(WinTitle?, WinText?, flags:=2, ...) => Gets monitor from window 41 | DPI.GetForMonitor(hMonitor, monitorDpiType := 0) => Gets the DPI for a specific monitor 42 | DPI.CoordsToScreen(&X, &Y, CoordMode, WinTitle?, ...) => Converts coordinates X and Y from CoordMode to screen coordinates 43 | DPI.CoordsToWindow(&X, &Y, CoordMode, WinTitle?, ...) => Converts coordinates X and Y from CoordMode to window coordinates 44 | DPI.CoordsToClient(&X, &Y, CoordMode, WinTitle?, ...) => Converts coordinates X and Y from CoordMode to client coordinates 45 | DPI.ScreenToWindow(&X, &Y, hWnd), ScreenToClient(&X, &Y, hWnd), WindowToClient(&X, &Y, hWnd), WindowToScreen(&X, &Y, hWnd), ClientToWindow(&X, &Y, hWnd), ClientToScreen(&X, &Y, hWnd) 46 | 47 | In addition, the following built-in functions are converted: 48 | DPI.MouseGetPos, MouseMove, MouseClick, MouseClickDrag, Click, WinGetPos, WinGetClientPos, PixelGetColor, PixelSearch, ImageSearch, 49 | ControlGetPos, ControlClick 50 | 51 | Notes: 52 | If AHK has launched a new thread (eg MsgBox) then any new pseudo-thread executing during that (eg user presses hotkey) might revert DPI back to system-aware. 53 | Setting DPI awareness for script process and monitoring WM_DPICHANGED message doesn't change that. Source: https://www.autohotkey.com/boards/viewtopic.php?p=310542#p310542 54 | For this reason, every Dpi function call that depends on coordinates automatically calls DPI.SetThreadDpiAwarenessContext(DPI.DefaultDpiAwarenessContext). This has a slight 55 | time cost equivalent to ~1 WinGetPos call. Overall, the Dpi functions are ~7-10x slower than the native ones, but still fast (<0.1ms per call in my setup). 56 | */ 57 | 58 | class DPI { 59 | 60 | static Standard := 96, WM_DPICHANGED := 0x02E0, MaximumPerMonitorDpiAwarenessContext := VerCompare(A_OSVersion, ">=10.0.15063") ? -4 : -3, DefaultDpiAwarenessContext := this.MaximumPerMonitorDpiAwarenessContext 61 | 62 | static __New() { 63 | ; Set DPI awareness of our script to maximum available per-monitor by default 64 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 65 | ; this.SetMaximumDPIAwareness(1) ; Also set the process DPI awareness? 66 | } 67 | 68 | /** 69 | * Gets the DPI for the specified window 70 | * @param WinTitle WinTitle, same as built-in 71 | * @param WinText 72 | * @param ExcludeTitle 73 | * @param ExcludeText 74 | * @returns {Integer} 75 | */ 76 | static GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 77 | /* 78 | ; The following only adds added complexity. GetDpiForWindow returns the correct DPI for windows that are monitor-aware. 79 | ; For system-aware or unaware programs it returns the FIRST DPI the window got initialized with, so if the window is dragged 80 | ; onto a second monitor then the DPI becomes invalid. Using MonitorFromWindow + GetForMonitor returns the correct DPI for both aware and unaware windows. 81 | context := DllCall("GetWindowDpiAwarenessContext", "ptr", hWnd, "ptr") 82 | if DllCall("GetAwarenessFromDpiAwarenessContext", "ptr", context, "int") = 2 ; If window is not DPI_AWARENESS_SYSTEM_AWARE 83 | return DllCall("GetDpiForWindow", "ptr", hWnd, "uint") 84 | ; Otherwise report the monitor DPI the window is currently located in 85 | */ 86 | local hMonitor, dpiX, dpiY 87 | return (hMonitor := DllCall("MonitorFromWindow", "ptr", WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), "int", 2, "ptr") ; MONITOR_DEFAULTTONEAREST 88 | , DllCall("Shcore.dll\GetDpiForMonitor", "ptr", hMonitor, "int", 0, "uint*", &dpiX:=0, "uint*", &dpiY:=0), dpiX) 89 | } 90 | 91 | /** 92 | * Returns an array of handles to monitors in order of monitors 93 | * @returns {Array} 94 | */ 95 | static GetMonitorHandles() { 96 | static EnumProc := CallbackCreate(MonitorEnumProc) 97 | local Monitors := [] 98 | return DllCall("User32\EnumDisplayMonitors", "Ptr", 0, "Ptr", 0, "Ptr", EnumProc, "Ptr", ObjPtr(Monitors), "Int") ? Monitors : false 99 | 100 | MonitorEnumProc(hMonitor, hDC, pRECT, ObjectAddr) { 101 | Monitors := ObjFromPtrAddRef(ObjectAddr) 102 | Monitors.Push(hMonitor) 103 | return true 104 | } 105 | } 106 | 107 | /** 108 | * Gets a monitor from coordinates (follows CoordMode) 109 | * @param {Integer} x DPI-adjusted x-coordinate 110 | * @param {Integer} y DPI-adjusted y-coordinate 111 | * @param {String} CoordMode The CoordMode to use (provide A_CoordModeMouse, A_CoordModePixel etc) 112 | * @param {number} flags Determines the function's return value if the point is not contained within any display monitor. 113 | * Defaults to nearest monitor. 114 | * MONITOR_DEFAULTTONULL = 0 => Returns 0. 115 | * MONITOR_DEFAULTTOPRIMARY = 1 => Returns a handle to the primary display monitor. 116 | * MONITOR_DEFAULTTONEAREST = 2 => Default. Returns a handle to the display monitor that is nearest to the point. 117 | * @returns {Integer} Handle to the monitor 118 | */ 119 | static MonitorFromPoint(x, y, CoordMode, flags:=2) { 120 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 121 | , this.FromStandardExceptCoordModeScreen(CoordMode, &X, &Y) 122 | , this.CoordsToScreen(&X, &Y, CoordMode, "A") 123 | return DllCall("MonitorFromPoint", "int64", y << 32 | (x & 0xFFFFFFFF), "int", flags, "ptr") 124 | } 125 | ; Gets a monitor from screen coordinates, no conversions done 126 | static MonitorFromPointRaw(x, y, flags:=2) { 127 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 128 | return DllCall("MonitorFromPoint", "int64", y << 32 | (x & 0xFFFFFFFF), "int", flags, "ptr") 129 | } 130 | 131 | /** 132 | * Gets a monitor from a window 133 | * @param WinTitle 134 | * @param WinText 135 | * @param {Integer} flags Determines the function's return value if the point is not contained within any display monitor. 136 | * Defaults to nearest monitor. 137 | * MONITOR_DEFAULTTONULL = 0 => Returns 0. 138 | * MONITOR_DEFAULTTOPRIMARY = 1 => Returns a handle to the primary display monitor. 139 | * MONITOR_DEFAULTTONEAREST = 2 => Default. Returns a handle to the display monitor that is nearest to the point. 140 | * @param ExcludeTitle 141 | * @param ExcludeText 142 | * @returns {Integer} 143 | */ 144 | static MonitorFromWindow(WinTitle?, WinText?, flags:=2, ExcludeTitle?, ExcludeText?) => DllCall("MonitorFromWindow", "int", WinExist(WinTitle?, WinText?, ExcludeText?, ExcludeText?), "int", flags, "ptr") 145 | 146 | /** 147 | * Returns the DPI for a certain monitor 148 | * @param {Integer} hMonitor Handle to the monitor (can be gotten with GetMonitorHandles) 149 | * @param {Integer} Monitor_Dpi_Type The type of DPI being queried. Can be one of the following: 150 | * MDT_EFFECTIVE_DPI = 0 => Default, the effective DPI. This value should be used when determining the correct scale factor for scaling UI elements. 151 | * MDT_ANGULAR_DPI = 1 => The angular DPI. This DPI ensures rendering at a compliant angular resolution on the screen. This does not include the scale factor set by the user for this specific display. 152 | * MDT_RAW_DPI = 2 => The raw DPI. This value is the linear DPI of the screen as measured on the screen itself. 153 | * @returns {Integer} 154 | */ 155 | static GetForMonitor(hMonitor, monitorDpiType := 0) { 156 | local dpiX, dpiY 157 | if !DllCall("Shcore\GetDpiForMonitor", "Ptr", hMonitor, "UInt", monitorDpiType, "UInt*", &dpiX:=0, "UInt*", &dpiY:=0, "UInt") 158 | return dpiX 159 | } 160 | 161 | static MouseGetPos(&OutputVarX?, &OutputVarY?, &OutputVarWin?, &OutputVarControl?, Flag?) { 162 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 163 | , MouseGetPos(&OutputVarX, &OutputVarY, &OutputVarWin, &OutputVarControl, Flag?) 164 | , this.ToStandardExceptCoordModeScreen(A_CoordModeMouse, &OutputVarX, &OutputVarY) 165 | } 166 | 167 | static MouseMove(X, Y, Speed?, Relative?) { 168 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 169 | , this.FromStandardExceptCoordModeScreen(A_CoordModeMouse, &X, &Y) 170 | , MouseMove(X, Y, Speed?, Relative?) 171 | } 172 | 173 | static MouseClick(WhichButton?, X?, Y?, ClickCount?, Speed?, DownOrUp?, Relative?) { 174 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 175 | , this.FromStandardExceptCoordModeScreen(A_CoordModeMouse, &X, &Y) 176 | , MouseClick(WhichButton?, X?, Y?, ClickCount?, Speed?, DownOrUp?, Relative?) 177 | } 178 | 179 | static MouseClickDrag(WhichButton?, X1?, Y1?, X2?, Y2?, Speed?, Relative?) { 180 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 181 | , this.FromStandardExceptCoordModeScreen(A_CoordModeMouse, &X, &Y) 182 | , MouseClickDrag(WhichButton?, X1?, Y1?, X2?, Y2?, Speed?, Relative?) 183 | } 184 | 185 | static Click(Options*) { 186 | local X, Y, regOut 187 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 188 | if Options.Length > 1 && IsInteger(Options[2]) { ; Click(x, y) 189 | this.FromStandardExceptCoordModeScreen(A_CoordModeMouse, &X:=Options[1], &Y:=Options[2]) 190 | , Options[1]:=X, Options[2]:=Y 191 | } else { ; Click("x y") 192 | if RegExMatch(Options[1], "i)\s*(\d+)\s+(\d+)", ®Out:="") { 193 | this.FromStandardExceptCoordModeScreen(A_CoordModeMouse, &X:=regOut[1], &Y:=regOut[2]) 194 | Options[1] := RegExReplace(Options[1], "i)(\d+)\s+(\d+)", X " " Y) 195 | } 196 | } 197 | Click(Options*) 198 | } 199 | 200 | ; Useful if window is moved to another screen after getting the position and size 201 | static WinGetPos(&X?, &Y?, &Width?, &Height?, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 202 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 203 | , WinGetPos(&X, &Y, &Width, &Height, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) 204 | , this.ToStandard(this.GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), &Width, &Height) 205 | } 206 | 207 | ; Useful if window is moved to another screen after getting the position and size 208 | static WinGetClientPos(&X?, &Y?, &Width?, &Height?, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 209 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 210 | , WinGetClientPos(&X, &Y, &Width, &Height, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) 211 | , this.ToStandard(this.GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), &Width, &Height) 212 | } 213 | 214 | static PixelGetColor(X, Y, Mode := '') { 215 | return (this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 216 | , this.FromStandardExceptCoordModeScreen(A_CoordModePixel, &X, &Y), PixelGetColor(X, Y, Mode)) 217 | } 218 | 219 | static PixelSearch(&OutputVarX, &OutputVarY, X1, Y1, X2, Y2, ColorID, Variation?) { 220 | local out 221 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 222 | , (out := PixelSearch(&OutputVarX, &OutputVarY, X1, Y1, X2, Y2, ColorID, Variation?)) && this.ToStandardExceptCoordModeScreen(A_CoordModePixel, &OutputVarX, &OutputVarY) 223 | return out 224 | } 225 | 226 | /** 227 | * ImageSearch that may work with all DPIs. Higher resolution images usually work better than lower resolution (that is, screenshot with high scaling) 228 | * OutputVarX, OutputVarY, X1, Y1, X2, Y2, ImageFile are same as native ImageSearch 229 | * @param OutputVarX 230 | * @param OutputVarY 231 | * @param X1 232 | * @param Y1 233 | * @param X2 234 | * @param Y2 235 | * @param ImageFile 236 | * @param dpi Allows specifying the "screen/window DPI". By default if CoordMode Pixel is Screen then A_ScreenDPI is used, otherwise the active windows' DPI 237 | * @param imgDpi Allows specifying the image DPI, since the DPI recorded in the image file is the one A_ScreenDPI was at the time of the taking. 238 | * In multi-monitor setups the main screen DPI is A_ScreenDPI, but if secondary screen DPI is different and image is captured there, then the wrong DPI is recorded (the primary monitors') 239 | * If imgDpi is a VarRef then it's set to the image DPI contained in image info. 240 | * @param imgW Gets set to the *w option value (eg the width of the image the search is actually performed with) 241 | * If screen/window DPI == image DPI then the actual size of the image is returned (since no scaling is necessary) 242 | * @param imgH Gets set to the *h option value (eg the height of the image the search is actually performed with) 243 | * @returns {number} 244 | */ 245 | static ImageSearch(&OutputVarX, &OutputVarY, X1, Y1, X2, Y2, ImageFile, dpi?, imgDpi?, &imgW?, &imgH?) { 246 | static oGdip := InitGdip() 247 | local ImgPath, pBitmap, regOut, out 248 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 249 | if !IsSet(dpi) 250 | dpi := (A_CoordModePixel = "screen") ? A_ScreenDPI : this.GetForWindow("A") 251 | 252 | ImgPath := RegExMatch(ImageFile, "i)(?: |^)(?!\*(?:icon|trans|w|h|)[-\d]+)(.+)", ®Out:="") ? regOut[1] : ImageFile 253 | if !InStr(ImgPath, "\") 254 | ImgPath := A_WorkingDir "\" ImgPath 255 | DllCall("gdiplus\GdipCreateBitmapFromFile", "uptr", StrPtr(ImgPath), "uptr*", &pBitmap:=0) 256 | if IsSet(imgDpi) && imgDpi is VarRef 257 | imgDpi := %imgDpi%, imgDpi := unset 258 | if !IsSet(imgDpi) 259 | DllCall("gdiplus\GdipGetImageHorizontalResolution", "uint", pBitmap, "float*", &imgDpi:=0), imgDpi := Round(imgDpi) 260 | 261 | if !RegExMatch(ImageFile, "i)\*w([-\d]+)\s+\*h([-\d]+)", ®Out:="") { 262 | DllCall("gdiplus\GdipGetImageWidth", "ptr", pBitmap, "uint*", &imgW:=0) 263 | DllCall("gdiplus\GdipGetImageHeight", "ptr", pBitmap, "uint*", &imgH:=0) 264 | if dpi != imgDpi 265 | this.ConvertCoord(&imgW, imgDpi, dpi), this.ConvertCoord(&imgH, imgDpi, dpi) 266 | } else 267 | imgW := Integer(regOut[1]), imgH := Integer(regOut[2]) 268 | 269 | DllCall("gdiplus\GdipDisposeImage", "ptr", pBitmap) 270 | 271 | if (out := ImageSearch(&OutputVarX, &OutputVarY, X1, Y1, X2, Y2, (dpi != imgDpi ? "*w" imgW " *h" imgH " " : "") ImageFile)) 272 | this.ToStandardExceptCoordModeScreen(A_CoordModePixel, &OutputVarX, &OutputVarY) 273 | return out 274 | 275 | InitGdip() { 276 | if (!DllCall("LoadLibrary", "str", "gdiplus", "UPtr")) 277 | throw Error("Could not load GDI+ library") 278 | 279 | local si := Buffer(A_PtrSize = 8 ? 24 : 16, 0), _oGdip := {}, pToken 280 | NumPut("UInt", 1, si) 281 | , DllCall("gdiplus\GdiplusStartup", "UPtr*", &pToken:=0, "UPtr", si.Ptr, "UPtr", 0) 282 | if (!pToken) 283 | throw Error("Gdiplus failed to start. Please ensure you have gdiplus on your system") 284 | _oGdip.DefineProp("ptr", {value:pToken}) 285 | , _oGdip.DefineProp("__Delete", {call:(this)=> DllCall("gdiplus\GdiplusShutdown", "Ptr", this.Ptr)}) 286 | return _oGdip 287 | } 288 | } 289 | 290 | static ControlGetPos(&OutX?, &OutY?, &OutWidth?, &OutHeight?, Control?, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 291 | local dpi 292 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 293 | , ControlGetPos(&OutX, &OutY, &OutWidth, &OutHeight, Control?, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) 294 | , this.ToStandard(dpi := this.GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), &OutX, &OutY) 295 | , this.ToStandard(dpi, &OutWidth, &OutHeight) 296 | } 297 | 298 | static ControlClick(ControlOrPos?, WinTitle?, WinText?, WhichButton?, ClickCount?, Options?, ExcludeTitle?, ExcludeText?) { 299 | local x, y, regOut 300 | this.SetThreadAwarenessContext(this.MaximumPerMonitorDpiAwarenessContext) 301 | if IsSet(ControlOrPos) && ControlOrPos is String { 302 | if RegExMatch(ControlOrPos, "i)x\s*(\d+)\s+y\s*(\d+)", ®Out:="") { 303 | this.FromStandard(this.GetForWindow(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), &x := Integer(regOut[1]), &y := Integer(regOut[2])) 304 | ControlOrPos := "X" x " Y" y 305 | } 306 | } 307 | ControlClick(ControlOrPos?, WinTitle?, WinText?, WhichButton?, ClickCount?, Options?, ExcludeTitle?, ExcludeText?) 308 | } 309 | 310 | ; Takes a GUI options string and converts all coordinates from fromDpi (default: DPI.Standard) to targetDpi 311 | ; Original author: user hi5, https://autohotkey.com/boards/viewtopic.php?f=6&t=37913 312 | static GuiOptScale(opt, targetDpi, fromDpi := this.Standard) { 313 | local number, out := "" 314 | Loop Parse, opt, A_Space A_Tab { 315 | if RegExMatch(A_LoopField,"i)(w0|h0|h-1|xp|yp|xs|ys|xm|ym)$|(icon|hwnd)") ; these need to be bypassed 316 | out .= A_LoopField A_Space 317 | else if RegExMatch(A_LoopField,"i)^\*?(x|xp|y|yp|w|h|s)[-+]?\K(\d+)", &number:="") ; should be processed 318 | out .= StrReplace(A_LoopField, number[2], this.ConvertCoord(&_:=Integer(number[2]), fromDpi, targetDpi)) A_Space 319 | else ; the rest can be bypassed as well (variable names etc) 320 | out .= A_LoopField A_Space 321 | } 322 | Return Trim(out) 323 | } 324 | 325 | static ToStandardExceptCoordModeScreen(CoordMode, &OutputVarX, &OutputVarY) => (CoordMode = "screen" || this.ToStandard(this.GetForWindow("A"), &OutputVarX, &OutputVarY)) 326 | static FromStandardExceptCoordModeScreen(CoordMode, &OutputVarX, &OutputVarY) => (CoordMode = "screen" || this.FromStandard(this.GetForWindow("A"), &OutputVarX, &OutputVarY)) 327 | static ConvertCoord(&coord, from, to) => ((IsNumber(coord) && coord := DllCall("MulDiv", "int", coord, "int", to, "int", from, "int")) || coord) 328 | 329 | ; Convert a point from standard to desired DPI, or vice-versa 330 | static FromStandard(dpi, &x, &y) => (IsInteger(x) && x := DllCall("MulDiv", "int", x, "int", dpi, "int", this.Standard, "int"), IsInteger(y) && y := DllCall("MulDiv", "int", y, "int", dpi, "int", this.Standard, "int")) 331 | static ToStandard(dpi, &x, &y) => (IsInteger(x) && x := DllCall("MulDiv", "int", x, "int", this.Standard, "int", dpi, "int"), IsInteger(y) && y := DllCall("MulDiv", "int", y, "int", this.Standard, "int", dpi, "int")) 332 | static GetScaleFactor(dpi) => Round(dpi / 96, 2) 333 | 334 | /** 335 | * Returns one of the following: 336 | * DPI_AWARENESS_INVALID = -1, 337 | * DPI_AWARENESS_UNAWARE = 0, 338 | * DPI_AWARENESS_SYSTEM_AWARE = 1, 339 | * DPI_AWARENESS_PER_MONITOR_AWARE = 2 340 | * @returns {Integer} 341 | */ 342 | static GetScriptAwareness() => DllCall("GetAwarenessFromDpiAwarenessContext", "ptr", DllCall("GetThreadDpiAwarenessContext", "ptr"), "int") 343 | 344 | /** 345 | * Uses DPI.SetThreadDpiAwarenessContext to set the running scripts' DPI awareness. Returns the previous context, but not in the same format as the 346 | * following context argument. 347 | * @param context May be one of the following values: 348 | * -1: DPI unaware. Automatically scaled by the system to system-dpi 349 | * -2: System DPI aware. Script queries for the DPI once and uses that value for the lifetime of the script. 350 | * If the DPI changes, the script will not adjust to the new DPI value. 351 | * -3: Per monitor DPI aware. Adjusts the scale factor whenever the DPI changes. 352 | * -4: Per Monitor v2. An advancement over the original per-monitor DPI awareness mode, which enables applications 353 | * to access new DPI-related scaling behaviors on a per top-level window basis. Dialogs, non-client areas and themes scale better. 354 | * -5: DPI unaware with improved quality of GDI-based content. 355 | * @returns {Integer} 356 | */ 357 | static SetThreadAwarenessContext(context) => DllCall("SetThreadDpiAwarenessContext", "ptr", context, "ptr") 358 | static SetProcessAwarenessContext(context) => DllCall("SetProcessDpiAwarenessContext", "ptr", context, "ptr") 359 | 360 | ; Converts coordinates to screen coordinates depending on provided CoordMode and window 361 | static CoordsToScreen(&X, &Y, CoordMode, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 362 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 363 | if CoordMode = "screen" { 364 | return 365 | } else if CoordMode = "client" { 366 | this.ClientToScreen(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 367 | } else { 368 | this.WindowToScreen(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 369 | } 370 | } 371 | ; Converts coordinates to client coordinates depending on provided CoordMode and window 372 | static CoordsToClient(&X, &Y, CoordMode, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 373 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 374 | if CoordMode = "screen" { 375 | this.ScreenToClient(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 376 | } else if CoordMode = "client" { 377 | return 378 | } else { 379 | this.WindowToClient(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 380 | } 381 | } 382 | ; Converts coordinates to window coordinates depending on provided CoordMode and window 383 | static CoordsToWindow(&X, &Y, CoordMode, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 384 | this.SetThreadAwarenessContext(this.DefaultDpiAwarenessContext) 385 | if CoordMode = "screen" { 386 | this.ScreenToWindow(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 387 | } else if CoordMode = "client" { 388 | this.ClientToWindow(&X, &Y, WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?)) 389 | } else { 390 | return 391 | } 392 | } 393 | 394 | static ClientToScreen(&x, &y, hWnd?) { 395 | local pt := Buffer(8) 396 | NumPut("int", x, "int", y, pt) 397 | , DllCall("ClientToScreen", "ptr", hWnd, "ptr", pt) 398 | , x := NumGet(pt, 0, "int"), y := NumGet(pt, 4, "int") 399 | } 400 | 401 | static ScreenToClient(&x, &y, hWnd?) { 402 | local pt := Buffer(8) 403 | NumPut("int", x, "int", y, pt) 404 | , DllCall("ScreenToClient", "ptr", hWnd, "ptr", pt) 405 | , x := NumGet(pt, 0, "int"), y := NumGet(pt, 4, "int") 406 | } 407 | 408 | static ScreenToWindow(&x, &y, hWnd?) { 409 | local RECT := Buffer(16,0) 410 | DllCall("user32\GetWindowRect", "Ptr", hWnd, "Ptr", RECT) 411 | x := x - NumGet(RECT, 0, "Int"), y := y - NumGet(RECT, 4, "Int") 412 | } 413 | 414 | static WindowToScreen(&x, &y, hWnd?) { 415 | local RECT := Buffer(16,0) 416 | DllCall("user32\GetWindowRect", "Ptr", hWnd, "Ptr", RECT) 417 | x := x + NumGet(RECT, 0, "Int"), y := y + NumGet(RECT, 4, "Int") 418 | } 419 | 420 | static ClientToWindow(&x, &y, hWnd?) { 421 | this.ClientToScreen(&x, &y, hWnd?) 422 | this.ScreenToWindow(&x, &y, hWnd?) 423 | } 424 | 425 | static WindowToClient(&x, &y, hWnd?) { 426 | this.WindowToScreen(&x, &y, hWnd?) 427 | this.ScreenToClient(&x, &y, hWnd?) 428 | } 429 | 430 | ; The following functions apparently do nothing 431 | 432 | static PhysicalToLogicalPointForPerMonitorDPI(&x, &y, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 433 | local pt64 := y << 32 | (x & 0xFFFFFFFF) 434 | DllCall("PhysicalToLogicalPointForPerMonitorDPI", "ptr", IsSet(WinTitle) && IsInteger(WinTitle) ? WinTitle : WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), "int64P", &pt64) 435 | x := 0xFFFFFFFF & pt64, y := pt64 >> 32 436 | } 437 | 438 | static LogicalToPhysicalPointForPerMonitorDPI(&x, &y, WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 439 | local pt := Buffer(8) 440 | NumPut("int", x, "int", y, pt) 441 | DllCall("LogicalToPhysicalPointForPerMonitorDPI", "ptr", IsSet(WinTitle) && IsInteger(WinTitle) ? WinTitle : WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), "ptr", pt) 442 | x := NumGet(pt, 0, "int"), y := NumGet(pt, 4, "int") 443 | } 444 | 445 | static AdjustWindowRectExForDpi(&x1, &y1, &x2, &y2, dpi) { 446 | local rect := Buffer(16) 447 | NumPut("int", x1, "int", y1, "int", x2, "int", y2, rect) 448 | DllCall("AdjustWindowRectExForDpi", "ptr", rect, "int", 0, "int", 0, "int", 0, "int", dpi) 449 | x1 := NumGet(rect, 0, "int"), y1 := NumGet(rect, 4, "int"), x2 := NumGet(rect, 8, "int"), y2 := NumGet(rect, 12, "int") 450 | } 451 | 452 | } -------------------------------------------------------------------------------- /Lib/DUnit.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | Name: DUnit.ahk 3 | Version 0.1 (13.10.22) 4 | Created: 07.10.22 5 | Author: Descolada 6 | 7 | Description: 8 | Unit testing library for AHK v2 9 | 10 | DUnit(TestClasses*) 11 | Tests the provided test classes sequentially: eq DUnit(TestSuite1, TestSuite2) 12 | 13 | A prototype TestClass: 14 | class TestSuite { 15 | static Fail() { ; Required static method if Coverage option is used, otherwise is optional. 16 | throw Error() 17 | } 18 | __New() { 19 | ; Ran before calling a test method from this class 20 | } 21 | Begin() { 22 | ; Ran before calling a test method from this class. Alternative to __New 23 | } 24 | __Delete() { 25 | ; Ran after calling a test method from this class. 26 | } 27 | End() { 28 | ; Ran after calling a test method from this class. Alternative to __Delete 29 | } 30 | Test_Func() { 31 | ; The methods to test. No specific name nomenclature required. The method cannot be static. 32 | } 33 | } 34 | 35 | DUnit methods: 36 | 37 | DUnit.True(a, msg := "Not True") 38 | Checks whether the condition a is not false (0, '', False). msg is the error message displayed. 39 | DUnit.False(a, msg := "Not False") 40 | Checks whether the condition a is false (0, '', False). msg is the error message displayed. 41 | DUnit.Equal(a, b, msg?, compareFunc?) 42 | Checks conditions a and b for equality. msg is the error message displayed. 43 | Optionally compareFunc(a,b) can be provided, otherwise Print(a) == Print(b) is used to check equality. 44 | DUnit.NotEqual(a, b, msg?, compareFunc?) 45 | Checks conditions a and b for non-equality. msg is the error message displayed. 46 | Optionally compareFunc(a,b) can be provided, otherwise Print(a) == Print(b) is used to check equality. 47 | DUnit.Assert(condition, msg := "Fail", n := -1) 48 | Functionally equivalent to DUnit.True 49 | n is the What argument for Error() 50 | DUnit.Throws(func, errorType?, msg := "FAIL (didn't throw)") 51 | Checks whether the provided func throws an error (optionally the type of the error is checked) 52 | DUnit.SetOptions(options?) 53 | Applies or resets DUnit options (see more below) 54 | 55 | DUnit properties/options: 56 | 57 | DUnit.Verbose 58 | Causes DUnit to also report successful tests by name. Otherwise only failed tests are reported. 59 | DUnit.FailFast 60 | Causes DUnit to return after the first encountered error. 61 | DUnit.Coverage 62 | Calculates the test coverage by the class. This option requires a static Fail() method 63 | to be implemented in the TestClass. Also the name of the script needs to start with "Test_" 64 | (that is, if the main script is Script.ahk then the one containing the test class should 65 | be named Test_Script.ahk). 66 | Notes about "Coverage": with long functions and multiple tests assertions in one test method 67 | this might fail to recognize some lines. Also when testing multiple testclasses in a row, then 68 | the autoexecute part of the testable file might not be recognized. 69 | "Coverage" is also quite slow, testing dozens-hundreds of methods might take seconds to even 70 | minutes. For this reason it is disabled by default. 71 | 72 | The properties can also be provided in the main DUnit() call as a string, and will 73 | be applied to all tests after the string: "Verbose"/"V", "FailFast"/"F", "Coverage"/"C" 74 | Example: DUnit("C", TestSuite) will apply Coverage option to TestSuite 75 | Options can also be applied with SetOptions(options?), where leaving options blank will reset 76 | to default options. 77 | */ 78 | 79 | class DUnit { 80 | static Verbose := "", FailFast := "", Coverage := "" 81 | /** 82 | * Applies DUnit options 83 | * @param options Space-separated options to apply: Verbose (V), FailFast (F), Coverage (C). If 84 | * left empty then will reset to default. 85 | */ 86 | static SetOptions(options?) { 87 | if IsSet(options) { 88 | for option in StrSplit(options, " ") { 89 | Switch option, 0 { 90 | case "V", "Verbose": 91 | DUnit.Verbose := True 92 | case "F", "FailFast": 93 | DUnit.FailFast := True 94 | case "C", "Coverage": 95 | DUnit.Coverage := True 96 | } 97 | } 98 | } else 99 | DUnit.Verbose := False, DUnit.FailFast := False, DUnit.Coverage := False 100 | } 101 | static __New() => DUnit.SetOptions() 102 | /** 103 | * New instance will test the provided testClasses, optionally also apply options 104 | * @param testClasses The classes to be tested 105 | * @example 106 | * DUnit(TestSuite1, "V", TestSuite2) ; tests two classes, and applies "Verbose" option for TestSuite2 107 | */ 108 | __New(testClasses*) { 109 | this.Print("Beginning unit testing:`n`n") 110 | totalFails := 0, totalSuccesses := 0, startTime := A_TickCount, GetCoverage := DUnit.Coverage 111 | ; First try to capture the auto-execute sections in all the classes 112 | for testClass in testClasses { 113 | if testClass is String { 114 | GetCoverage := InStr(testClass, "C") 115 | continue 116 | } 117 | testClass() 118 | if GetCoverage 119 | DUnit.GetCoverage(testClass) 120 | } 121 | ; Start the testing for real 122 | for testClass in testClasses { 123 | ; If there are any options provided, reset all options and apply new ones. 124 | if testClass is String { 125 | DUnit.SetOptions(testClass) 126 | continue 127 | } 128 | fails := 0, successes := 0, currentListLines := A_ListLines, coverage := 0 129 | this.Print(Type(testClass()) ": ") 130 | if DUnit.Coverage && !currentListLines 131 | ListLines 1 132 | ; Test all methods in testClass sequentially 133 | for test in ObjOwnProps(testClass.Prototype) { 134 | ; Reset environment so one test doesn't affect another 135 | env := testClass() 136 | ; Ignore init/cleanup methods, and Fail (which is only required for "Coverage") 137 | if !(test ~= "i)^Begin$|^End$|^Fail$") and SubStr(test, 1, 2) != '__' { 138 | if env.base.HasOwnProp("Begin") 139 | env.Begin() 140 | try 141 | env.%test%() 142 | catch as e { 143 | fails++ 144 | this.Print("`nFAIL: " Type(env) "." test "`n" StrReplace(e.File, A_InitialWorkingDir "\") " (" e.Line ") : " e.Message) 145 | if DUnit.FailFast 146 | break 147 | } else { 148 | successes++ 149 | if DUnit.Verbose 150 | this.Print("`nSuccess: " Type(env) "." test) 151 | } 152 | if env.base.HasOwnProp("End") 153 | env.End() 154 | if DUnit.Coverage 155 | coverage := DUnit.GetCoverage(testClass) 156 | } 157 | env := "" 158 | } 159 | if DUnit.Coverage && !currentListLines 160 | ListLines 0 161 | if !fails { 162 | this.Print(DUnit.Verbose ? "`nAll pass." : "all pass.") 163 | } else { 164 | if DUnit.FailFast { 165 | this.Print("`n`n") 166 | break 167 | } 168 | } 169 | if DUnit.Coverage 170 | this.Print("`n---- Test coverage: " coverage "% ----") 171 | this.Print("`n`n") 172 | totalFails += fails, totalSuccesses += successes 173 | } 174 | this.Print("=========================`nTotal " (totalFails+totalSuccesses) " tests in " Round((A_TickCount-startTime)/1000, 3) "s: " totalSuccesses " successes, " totalFails " fails.") 175 | } 176 | /** 177 | * Checks whether the condition a is True 178 | * @param a Condition (value) to check 179 | * @param msg Optional: error message to display 180 | */ 181 | static True(a, msg := "Not True") => DUnit.Assert(a, msg, -2) 182 | /** 183 | * Checks whether the condition a is False 184 | * @param a Condition (value) to check 185 | * @param msg Optional: error message to display 186 | */ 187 | static False(a, msg := "Not False") => DUnit.Assert(!a, msg, -2) 188 | /** 189 | * Checks whether two conditions/values are equal 190 | * @param a First condition 191 | * @param b Second condition 192 | * @param msg Optional: error message to display 193 | * @param compareFunc Optional: the function used to compare the values. By default PrintString() is used. 194 | */ 195 | static Equal(a, b, msg?, compareFunc?) { 196 | currentListLines := A_ListLines 197 | ListLines 0 198 | if IsSet(compareFunc) && HasMethod(compareFunc) 199 | DUnit.Assert(compareFunc(a,b), msg ?? "Not equal", -2) 200 | else 201 | DUnit.Assert((pa := DUnit.PrintString(a)) == (pb := DUnit.PrintString(b)), msg ?? pa ' != ' pb, -2) 202 | ListLines currentListLines 203 | } 204 | /** 205 | * Checks whether two conditions/values are not equal 206 | * @param a First condition 207 | * @param b Second condition 208 | * @param msg Optional: error message to display 209 | * @param compareFunc Optional: the function used to compare the values. By default PrintString() is used. 210 | */ 211 | static NotEqual(a, b, msg?, compareFunc?) { 212 | currentListLines := A_ListLines 213 | ListLines 0 214 | if IsSet(compareFunc) && HasMethod(compareFunc) 215 | DUnit.Assert(!compareFunc(a,b), msg ?? "Are equal", -2) 216 | else 217 | DUnit.Assert((pa := DUnit.PrintString(a)) != (pb := DUnit.PrintString(b)), msg ?? pa ' == ' pb, -2) 218 | ListLines currentListLines 219 | } 220 | /** 221 | * Checks whether the condition is not 0, "", or False 222 | * @param a Condition (value) to check 223 | * @param msg Optional: error message to display 224 | * @param n Optional: Error message What argument. Default is -1. 225 | */ 226 | static Assert(condition, msg := "Fail", n := -1) { 227 | if !condition 228 | throw Error(msg?, n) 229 | } 230 | /** 231 | * Checks whether the condition (function) throws an error when called. 232 | * @param func The function to test 233 | * @param errorType Optional: checks whether a specific error type needs to be thrown 234 | * @param msg Optional: Error message to show 235 | */ 236 | static Throws(func, errorType?, msg := "FAIL (didn't throw)") { 237 | try 238 | func() 239 | catch as e { 240 | if IsSet(errorType) && (Type(e) != errorType) 241 | DUnit.Assert(False, msg) 242 | return 243 | } 244 | DUnit.Assert(false, msg) 245 | } 246 | 247 | /** 248 | * Internal method used to print out the results. 249 | */ 250 | Print(value) { 251 | Result := DUnit.Print() 252 | if HasMethod(Result[1]) 253 | Result[1].Call(value) 254 | else 255 | OutputDebug(value) 256 | } 257 | 258 | /** 259 | * Internal method used to calculate the tested scripts' effective line-count. 260 | * It excludes the comments, empty lines, nonfunctional lines ("else", "get", "class" etc), etc. 261 | * @param file The full path of the file to be analyzed 262 | */ 263 | static GetScriptEffectiveLineCount(file) { 264 | static files := Map() 265 | if files.Has(file) 266 | return files[file] 267 | fileContent := FileRead(file) 268 | 269 | fileContent := RegExRemove(fileContent, "\/\*[\w\W]*?\*\/") ; remove block comments 270 | fileContent := RegExRemove(fileContent, "m)^[ \t]*;.*\s*") ; remove single comments 271 | fileContent := RegExRemove(fileContent, "m)^[ \t\{\}\(\)]*(\s+|$)") ; remove empty lines or ones with brackets 272 | fileContent := RegexRemove(fileContent, "im)[ \t]*\}?[ \t]*(else|get|set|class .*)[ \t]*\{?(\s+|$)") 273 | fileContent := StrSplit(fileContent, "`n") 274 | ; For debugging reasons could display fileContent to see the remaining parts of the script 275 | return files[file] := fileContent.Length 276 | 277 | RegExRemove(Haystack, Needle) { 278 | count := 0 279 | Haystack := RegExReplace(Haystack, Needle,, &count:=0) 280 | While count 281 | Haystack := RegExReplace(Haystack, Needle,, &count:=0) 282 | return Haystack 283 | } 284 | } 285 | 286 | /** 287 | * Internal function used to calculate the coverage and keep track of already visited lines. 288 | * This needs to be called multiple times after visitid each method in testClass, because 289 | * ListLines can "overflow" and thus some visited lines might be missed. 290 | * @param testClass The class currently being tested. 291 | */ 292 | static GetCoverage(testClass) { 293 | currentListLines := A_ListLines 294 | ListLines 0 295 | static knownClasses := Map() 296 | classType := Type(testClass()) 297 | if !knownClasses.Has(classType) { 298 | ; First find the testfile name, which in turn will give us the name of the actual file. 299 | ; If trying to analyze Script.ahk, then Fail() will give us Test_Script.ahk, then we know 300 | ; what to look for. 301 | if !ObjHasOwnProp(testClass, "Fail") 302 | throw Error("The class " Type(classType) " doesn't have the necessary static Fail method implemented.", -1) 303 | try testClass.Fail() 304 | catch as e { 305 | testLine := e.Line, testFile := e.File 306 | } 307 | SplitPath testFile, &file 308 | if !SubStr(file, 1, 5) = "Test_" 309 | throw Error("Lib file corresponding to '" file "' not found!") 310 | file := SubStr(file, 6) ; Trim away "Test_" 311 | lines := DUnit.ScriptInfo("ListLines") 312 | ; Try to find the corresponding file in ListLines 313 | if RegExMatch(lines, "i)---- (.*\Q\" file "\E)\s+", &match) 314 | filePath := match[1] 315 | else 316 | throw Error("Corresponding script file to '" file "' not found in ListLines") 317 | if !FileExist(filePath) 318 | throw Error("Script file not found at '" filePath "'") 319 | 320 | ; Now that we have the file, calculate the effective line count (trim away comments, empty lines etc) 321 | effectiveLineCount := DUnit.GetScriptEffectiveLineCount(filePath) 322 | thisClass := knownClasses[classType] := Map() 323 | thisClass["LineCount"] := effectiveLineCount 324 | thisClass["FilePath"] := filePath 325 | } else 326 | lines := DUnit.ScriptInfo("ListLines"), thisClass := knownClasses[classType] 327 | 328 | startPos := 1, executedLines := "" 329 | While startPos := RegExMatch(lines, "im)^---- \Q" thisClass["FilePath"] "\E\s+([\w\W]*?)\n---- ", &match, startPos) { 330 | ;MsgBox("Found " match.Count " matches at " startPos " + " StrLen(match[0])) ; debugging line 331 | ;MsgBox(match[0]) ; debugging line 332 | executedLines .= match[1] "`n" 333 | for i, val in StrSplit(match[1], "`n") { 334 | if RegExMatch(val, "^(\d+):", &lineNum:="") ; verify that the line has a line number 335 | thisClass[Integer(lineNum[1])] := 1 ; map the line number 336 | } 337 | startPos += StrLen(match[]) 338 | } 339 | ListLines currentListLines 340 | return Round(((thisClass.Count - 2) / thisClass["LineCount"]) * 100, 1) ; subtract LineCount and FilePath 341 | } 342 | 343 | /** 344 | * Prints the formatted value of a variable (number, string, object). 345 | * Leaving all parameters empty will return the current function and newline in an Array: [func, newline] 346 | * @param value Optional: the variable to print. 347 | * If omitted then new settings (output function and newline) will be set. 348 | * If value is an object/class that has a ToString() method, then the result of that will be printed. 349 | * @param func Optional: the print function to use. Default is OutputDebug. 350 | * Not providing a function will cause the output to simply be returned as a string. 351 | * @param newline Optional: the newline character to use (applied to the end of the value). 352 | * Default is newline (`n). 353 | */ 354 | static Print(value?, func?, newline?) { 355 | static p := OutputDebug, nl := "" 356 | if IsSet(func) 357 | p := func 358 | if IsSet(newline) 359 | nl := newline 360 | if IsSet(value) { 361 | val := this.PrintString(value) nl 362 | return HasMethod(p) ? p(val) : val 363 | } 364 | return [p, nl] 365 | } 366 | 367 | static PrintString(val?) { 368 | if !IsSet(val) 369 | return "unset" 370 | valType := Type(val) 371 | switch valType, 0 { 372 | case "String": 373 | return "'" StrReplace(StrReplace(StrReplace(val, "`n", "``n"), "`r", "``r"), "`t", "``t") "'" 374 | case "Integer", "Float": 375 | return val 376 | default: 377 | self := "", iter := "", out := "" 378 | try self := this.PrintString(val.ToString()) ; if the object has ToString available, print it 379 | if valType != "Array" { ; enumerate object with key and value pair, except for array 380 | try { 381 | enum := val.__Enum(2) 382 | while (enum.Call(&val1, &val2)) 383 | iter .= this.PrintString(val1) ":" this.PrintString(val2?) ", " 384 | } 385 | } 386 | if !IsSet(enum) { ; if enumerating with key and value failed, try again with only value 387 | try { 388 | enum := val.__Enum(1) 389 | while (enum.Call(&enumVal)) 390 | iter .= this.PrintString(enumVal?) ", " 391 | } 392 | } 393 | if !IsSet(enum) && (valType = "Object") && !self { ; if everything failed, enumerate Object props 394 | for k, v in val.OwnProps() 395 | iter .= SubStr(this.PrintString(k), 2, -1) ":" this.PrintString(v?) ", " 396 | } 397 | iter := SubStr(iter, 1, StrLen(iter)-2) 398 | if !self && !iter && !((valType = "Array" && val.Length = 0) || (valType = "Map" && val.Count = 0) || (valType = "Object" && ObjOwnPropCount(val) = 0)) 399 | return valType ; if no additional info is available, only print out the type 400 | else if self && iter 401 | out .= "value:" self ", iter:[" iter "]" 402 | else 403 | out .= self iter 404 | return (valType = "Object") ? "{" out "}" : (valType = "Array") ? "[" out "]" : valType "(" out ")" 405 | } 406 | } 407 | 408 | /** 409 | * Returns the text that would have been shown in AutoHotkey's main window if you had called Command, 410 | * but without actually showing or activating the window. 411 | * @param Command must be "ListLines", "ListVars", "ListHotkeys" or "KeyHistory" 412 | * @author Lexikos 413 | */ 414 | static ScriptInfo(Command) { 415 | static hEdit := 0, pfn, bkp, cmds := {ListLines:65406, ListVars:65407, ListHotkeys:65408, KeyHistory:65409} 416 | if !hEdit { 417 | hEdit := DllCall("GetWindow", "ptr", A_ScriptHwnd, "uint", 5, "ptr") 418 | user32 := DllCall("GetModuleHandle", "str", "user32.dll", "ptr") 419 | pfn := [], bkp := [] 420 | for i, fn in ["SetForegroundWindow", "ShowWindow"] { 421 | pfn.Push(DllCall("GetProcAddress", "ptr", user32, "astr", fn, "ptr")) 422 | DllCall("VirtualProtect", "ptr", pfn[i], "ptr", 8, "uint", 0x40, "uint*", 0) 423 | bkp.Push(NumGet(pfn[i], 0, "int64")) 424 | } 425 | } 426 | 427 | if (A_PtrSize=8) { ; Disable SetForegroundWindow and ShowWindow. 428 | NumPut("int64", 0x0000C300000001B8, pfn[1], 0) ; return TRUE 429 | NumPut("int64", 0x0000C300000001B8, pfn[2], 0) ; return TRUE 430 | } else { 431 | NumPut("int64", 0x0004C200000001B8, pfn[1], 0) ; return TRUE 432 | NumPut("int64", 0x0008C200000001B8, pfn[2], 0) ; return TRUE 433 | } 434 | 435 | cmds.%Command% ? DllCall("SendMessage", "ptr", A_ScriptHwnd, "uint", 0x111, "ptr", cmds.%Command%, "ptr", 0) : 0 436 | 437 | NumPut("int64", bkp[1], pfn[1], 0) ; Enable SetForegroundWindow. 438 | NumPut("int64", bkp[2], pfn[2], 0) ; Enable ShowWindow. 439 | 440 | return ControlGetText(hEdit) 441 | } 442 | } -------------------------------------------------------------------------------- /Lib/Editable.ahk: -------------------------------------------------------------------------------- 1 | #Requires Autohotkey v2.0+ 2 | #Include ..\UIA-v2\Lib\UIA.ahk 3 | toc := " 4 | ( 5 | Table of contents: 6 | 7 | F1: inserts 'text' at the caret 8 | F2: displays the current selection 9 | F3: selects characters 4-10 10 | F4: runs Notepad, inserts 'ipsum' into the text 11 | F6: displays the caret offset 12 | )" 13 | MsgBox(toc) 14 | F1:: 15 | { 16 | el := Editable() 17 | el.Insert("test") 18 | ToolTip "Inserted 'test' into: " el.Dump() 19 | SetTimer(ToolTip, -3000) 20 | } 21 | F2:: 22 | { 23 | selection := Editable().Selection 24 | ToolTip "Text: " selection.text "`nStart: " selection.start "`nEnd: " selection.end 25 | SetTimer(ToolTip, -3000) 26 | } 27 | F3:: 28 | { 29 | Editable().Select(4, 10) 30 | } 31 | F4:: 32 | { 33 | winTitle := "ahk_exe notepad.exe" 34 | if WinExist(winTitle) 35 | WinActivate winTitle 36 | else 37 | Run "notepad.exe" 38 | WinWaitActive winTitle 39 | element := UIA.ElementFromHandle().FindElement({IsTextPatternAvailable:1}) 40 | if !element.Value 41 | element.Value := "Lorem dolor" 42 | Editable(element).Insert(" ipsum", 5) 43 | } 44 | F6:: 45 | { 46 | ToolTip Editable().CaretOffset 47 | SetTimer(ToolTip, -3000) 48 | } 49 | 50 | /** 51 | * Editable class can be used to interact with editable elements such as textboxes and manipulate their content. 52 | * Editable extends IUIAutomationElement class, so all normal UIA element methods can be used with the returned object. 53 | * 54 | * Editable() => creates a new instance for the element with the caret (selected element) 55 | * Editable(element) => creates a new instance for an editable UIA element 56 | * 57 | * Editable class static methods: 58 | * Editable.GetCaretPos(&x:=0, &y:=0) => returns the current caret screen position coordinates 59 | * Editable.NearestEditableElement(element) => returns an editable element which is either the element itself, a child, or a parent 60 | * 61 | * Editable class instance properties: 62 | * Editable().Value => gets or sets the current text 63 | * Editable().Selection => returns an object {start, end, text} with the current selection text, and start and end offsets 64 | * Editable().CaretOffset => returns the position of the caret relative to the beginning of the text 65 | * 66 | * Editable class instance methods: 67 | * Editable().Select(from, to) => selects text from offsets (starting from beginning of the element) 68 | * Editable().Insert(text, offset?) => inserts text, by default to the current caret position 69 | */ 70 | class Editable extends UIA.IUIAutomationElement { 71 | static __pLib := DllCall("LoadLibrary", "str", "oleacc") 72 | static __Delete => DllCall("FreeLibrary", "ptr", Editable.__pLib) 73 | 74 | /** 75 | * Creates a new Editable instance, either from the element with caret, or a provided UIA element. 76 | * UIAutomation is activated for Chromium windows automatically. 77 | * @returns {Editable} 78 | */ 79 | __New(element?) { 80 | static activatedHwnds := Map(), OBJID_WINDOW := 0x00000000, OBJID_CLIENT := 0xfffffffc, OBJID_CARET := 0xfffffff8, WM_GETOBJECT := 0x003D, IID := Buffer(16) 81 | hWnd := WinExist("A"), cHwnd := 0, pLib := Editable.__pLib, this.DefineProp("IsIAccessible2", {value:0}) 82 | if IsSet(element) { 83 | target := Editable.NearestEditableElement(element) 84 | this.DefineProp("ptr", {value:target.ptr}), target.AddRef() 85 | try { 86 | this.DefineProp("IAccessible", {value:this.GetIAccessible()}) 87 | DllCall("oleacc\WindowFromAccessibleObject", "Ptr", this.IAccessible, "uint*", &hWnd:=0) 88 | hWnd := DllCall("GetAncestor", "UInt", hWnd, "UInt", 2) 89 | cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", hWnd) 90 | this.DefineProp("IAccessible2Text", {value:ComObjQuery(ComObjValue(this.IAccessible), "{618736e0-3c3d-11cf-810c-00aa00389b71}", "{24FD2FFB-3AAD-4A08-8335-A3AD89C0FB4B}")}) ; IAccessibleText 91 | this.IsIAccessible2 := cHwnd 92 | } 93 | return 94 | } 95 | try cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", hWnd) 96 | if cHwnd 97 | UIA.ActivateChromiumAccessibility(hWnd) 98 | 99 | Editable.GetCaretPos(&x, &y) 100 | target := Editable.NearestEditableElement(UIA.ElementFromPoint(x, y)) 101 | this.DefineProp("ptr", {Value:target.ptr}) 102 | target.AddRef() 103 | try { 104 | this.DefineProp("IAccessible", {value:this.GetIAccessible()}) 105 | this.DefineProp("IAccessible2Text", {value:ComObjQuery(ComObjValue(this.IAccessible), "{618736e0-3c3d-11cf-810c-00aa00389b71}", "{24FD2FFB-3AAD-4A08-8335-A3AD89C0FB4B}")}) ; IAccessibleText 106 | this.IsIAccessible2 := hWnd 107 | } 108 | } 109 | 110 | ; Returns IUIAutomationTextRange for the current selection 111 | SelectionRange { 112 | get => this.TextPattern.GetSelection()[1] 113 | } 114 | 115 | ; Returns an object {start, end, text} with the current selection text, and start and end offsets 116 | Selection { 117 | get { 118 | if this.IsIAccessible2 { 119 | try { 120 | ComCall(9, this.IAccessible2Text, "int", 0, "int*", &startOffset:=0, "int*", &endOffset:=0) ; Selection 121 | ComCall(10, this.IAccessible2Text, "int", startOffset, "int", endOffset, "ptr*", &text:=0) ; Text 122 | return {start:startOffset, end:endOffset, text:UIA.BSTR(text)} 123 | } 124 | } 125 | selection := this.SelectionRange, offset := this.CaretOffset 126 | return {start:offset, end:offset+selection.CompareEndpoints(UIA.TextPatternRangeEndpoint.End, selection, UIA.TextPatternRangeEndpoint.Start), text:selection.GetText()} 127 | } 128 | } 129 | 130 | /** 131 | * Selects text from offsets, starting from beginning of the element. 132 | * @param from The starting offset 133 | * @param to The ending offset 134 | */ 135 | Select(from, to) { 136 | if this.IsIAccessible2 { 137 | ComCall(16, this.IAccessible2Text, "int", 0, "int", from, "int", to) 138 | } else { 139 | doc := this.DocumentRange 140 | doc.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.End, UIA.TextUnit.Document, -1) 141 | doc.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.End, UIA.TextUnit.Character, to) 142 | doc.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.Start, UIA.TextUnit.Character, from) 143 | doc.Select() 144 | } 145 | } 146 | 147 | ; Returns the caret offset from the beginning of the element 148 | CaretOffset { 149 | get { 150 | if this.IsIAccessible2 { 151 | ComCall(5, this.IAccessible2Text, "int*", &offset:=0) ; CaretOffset 152 | return offset 153 | } else { 154 | selected := this.TextPattern.GetSelection()[1] 155 | document := this.DocumentRange 156 | offset := selected.CompareEndpoints(UIA.TextPatternRangeEndpoint.Start, document, UIA.TextPatternRangeEndpoint.Start) 157 | if offset != 1 158 | return offset 159 | ; This is required pretty much only for Edge 160 | timeOut := A_TickCount+500, counter := 0 161 | While A_TickCount < timeOut { 162 | if offset = 0 163 | break 164 | document.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.Start, UIA.TextUnit.Character, 1) 165 | counter++ 166 | offset := selected.CompareEndpoints(UIA.TextPatternRangeEndpoint.Start, document, UIA.TextPatternRangeEndpoint.Start) 167 | } 168 | if A_TickCount >= timeOut 169 | throw TimeoutError("Getting caret offset timed out", -1) 170 | return counter 171 | } 172 | } 173 | set { 174 | if this.IsIAccessible2 175 | ComCall(15, this.IAccessible2Text, "int", Value) 176 | else 177 | this.Select(Value, Value) 178 | } 179 | } 180 | 181 | /** 182 | * Inserts text, by default to the current caret position. The caret is moved to the end of the inserted text. 183 | * Returns an object: {oldText, newText, caretOffset} 184 | * @param text The text to insert 185 | * @param offset Optional: the offset from the beginning of the element. Default is caret position 186 | * @returns {Object} 187 | */ 188 | Insert(text, offset?) { 189 | if this.IsIAccessible2 { 190 | if !IsSet(offset) 191 | ComCall(5, this.IAccessible2Text, "int*", &offset:=0) ; CaretOffset 192 | curVal := this.IAccessible.accValue[0], newVal := SubStr(curVal, 1, offset) text SubStr(curVal, offset+1) 193 | this.IAccessible.accValue[0] := newVal 194 | Sleep 1 195 | ComCall(15, this.IAccessible2Text, "int", StrLen(SubStr(curVal, 1, offset) text)) 196 | } else { 197 | selection := this.TextPattern.GetSelection()[1] 198 | if !IsSet(offset) 199 | offset := this.CaretOffset 200 | curVal := this.Value, newVal := SubStr(curVal, 1, offset) text SubStr(curVal, offset+1) 201 | this.Value := newVal 202 | selection.Move(UIA.TextUnit.Character, StrLen(SubStr(curVal, 1, offset) text)) 203 | selection.Select() 204 | } 205 | return {oldText:curVal, newText:newVal, caretOffset:offset} 206 | } 207 | 208 | /** 209 | * Returns the caret screen position 210 | * @param x Is set to the x coordinate 211 | * @param y Is set to the y coordinate 212 | */ 213 | static GetCaretPos(&x:=0, &y:=0) { 214 | static OBJID_CARET := 0xfffffff8, IID := Buffer(16) 215 | 216 | ; Get IAccessible for Caret 217 | if DllCall("oleacc\AccessibleObjectFromWindow", "ptr", WinExist("A"), "uint", OBJID_CARET 218 | , "ptr",-16 + NumPut("int64", 0x719B3800AA000C81, NumPut("int64", 0x11CF3C3D618736E0, IID)) 219 | , "ptr*", oCaret := ComValue(9,0)) != 0 220 | throw Error("Unable to get caret IAccessible", -1) 221 | 222 | ; Get caret position 223 | x:=Buffer(4, 0), y:=Buffer(4, 0), w:=Buffer(4, 0), h:=Buffer(4, 0) 224 | oCaret.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), 0) 225 | x := NumGet(x, 0, "int"), y := NumGet(y, 0, "int") 226 | if !x && !y { 227 | savedCaret := A_CoordModeCaret 228 | CoordMode "Caret", "Screen" 229 | CaretGetPos(&x, &y) 230 | CoordMode "Caret", savedCaret 231 | } 232 | if !x && !y { 233 | try { 234 | focused := UIA.GetFocusedElement() 235 | range := Editable.NearestEditableElement(focused).TextPattern.GetSelection()[1] 236 | rect := range.GetBoundingRectangles()[1] 237 | x := rect.x, y := rect.y 238 | } 239 | } 240 | if !x && !y 241 | throw Error("Unable to get the caret position", -1) 242 | } 243 | 244 | /** 245 | * Returns the nearest editable element to the element. First the element itself is checked 246 | * for editability, next the descendants are searched for an editable element, 247 | * finally the parents are searched. 248 | * @param element The element to start the search with 249 | * @returns {UIA.IUIAutomationElement} 250 | */ 251 | static NearestEditableElement(element) { 252 | if element.IsTextPatternAvailable 253 | return element 254 | try return element.FindElement({IsTextPatternAvailable:1}) 255 | catch { 256 | try return UIA.CreateTreeWalker({IsTextPatternAvailable:1}).GetParentElement(element) 257 | catch 258 | throw TargetError("TextPattern not available for the element", -2) 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /Lib/Map.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | Name: Map.ahk 3 | Version 0.1 (05.09.23) 4 | Created: 05.09.23 5 | Author: Descolada 6 | 7 | Description: 8 | A compilation of useful Map methods. 9 | 10 | Map.Keys => All keys of the map in an array 11 | Map.Values => All values of the map in an array 12 | Map.Map(func, enums*) => Applies a function to each element in the map. 13 | Map.ForEach(func) => Calls a function for each element in the map. 14 | Map.Filter(func) => Keeps only key-value pairs that satisfy the provided function 15 | Map.Reduce(func, initialValue?) => Applies a function cumulatively to all the values in 16 | the array, with an optional initial value. 17 | Map.Find(func, &match?, start:=1) => Finds a value satisfying the provided function and returns the index. 18 | match will be set to the found value. 19 | Map.Count(value) => Counts the number of occurrences of a value. 20 | Map.Merge(enums*) => Adds the contents of other maps/enumerables to this map 21 | */ 22 | 23 | class Map2 { 24 | static __New() => (Map2.base := Map.Prototype.base, Map.Prototype.base := Map2) 25 | /** 26 | * Returns all the keys of the Map in an array 27 | * @returns {Array} 28 | */ 29 | static Keys { 30 | get => [this*] 31 | } 32 | /** 33 | * Returns all the values of the Map in an array 34 | * @returns {Array} 35 | */ 36 | static Values { 37 | get => [this.__Enum(2).Bind(&_)*] 38 | } 39 | 40 | /** 41 | * Applies a function to each element in the map (mutates the map). 42 | * @param func The mapping function that accepts at least key and value (key, value1, [value2, ...]). 43 | * @param enums Additional enumerables to be accepted in the mapping function 44 | * @returns {Map} 45 | */ 46 | static Map(func, enums*) { 47 | if !HasMethod(func) 48 | throw ValueError("Map: func must be a function", -1) 49 | for k, v in this { 50 | bf := func.Bind(k,v) 51 | for _, vv in enums 52 | bf := bf.Bind(vv.Has(k) ? vv[k] : unset) 53 | try bf := bf() 54 | this[k] := bf 55 | } 56 | return this 57 | } 58 | /** 59 | * Applies a function to each key/value pair in the map. 60 | * @param func The callback function with arguments Callback(value[, key, map]). 61 | * @returns {Map} 62 | */ 63 | static ForEach(func) { 64 | if !HasMethod(func) 65 | throw ValueError("ForEach: func must be a function", -1) 66 | for i, v in this 67 | func(v, i, this) 68 | return this 69 | } 70 | /** 71 | * Keeps only values that satisfy the provided function 72 | * @param func The filter function that accepts key and value. 73 | * @returns {Map} 74 | */ 75 | static Filter(func) { 76 | if !HasMethod(func) 77 | throw ValueError("Filter: func must be a function", -1) 78 | r := Map() 79 | for k, v in this 80 | if func(k, v) 81 | r[k] := v 82 | return this := r 83 | } 84 | /** 85 | * Finds a value satisfying the provided function and returns its key. 86 | * @param func The condition function that accepts one argument (value). 87 | * @param match Optional: is set to the found value 88 | * @example 89 | * Map("a", 1, "b", 2, "c", 3).Find((v) => (Mod(v,2) == 0)) ; returns "b" 90 | */ 91 | static Find(func, &match?) { 92 | if !HasMethod(func) 93 | throw ValueError("Find: func must be a function", -1) 94 | for k, v in this { 95 | if func(v) { 96 | match := v 97 | return k 98 | } 99 | } 100 | return 0 101 | } 102 | /** 103 | * Counts the number of occurrences of a value 104 | * @param value The value to count. Can also be a function that accepts a value and evaluates to true/false. 105 | */ 106 | static Count(value) { 107 | count := 0 108 | if HasMethod(value) { 109 | for _, v in this 110 | if value(v?) 111 | count++ 112 | } else 113 | for _, v in this 114 | if v == value 115 | count++ 116 | return count 117 | } 118 | /** 119 | * Adds the contents of other enumerables to this one. 120 | * @param enums The enumerables that are used to extend this one. 121 | * @returns {Array} 122 | */ 123 | static Merge(enums*) { 124 | for i, enum in enums { 125 | if !HasMethod(enum, "__Enum") 126 | throw ValueError("Extend: argument " i " is not an iterable") 127 | for k, v in enum 128 | this[k] := v 129 | } 130 | return this 131 | } 132 | } -------------------------------------------------------------------------------- /Lib/Sync.ahk: -------------------------------------------------------------------------------- 1 | class Mutex { 2 | /** 3 | * Creates a new Mutex, or opens an existing one. The mutex is destroyed once all handles to 4 | * it are closed. 5 | * @param name Optional. The name can start with "Local\" to be session-local, or "Global\" to be 6 | * available system-wide. 7 | * @param initialOwner Optional. If this value is TRUE and the caller created the mutex, the 8 | * calling thread obtains initial ownership of the mutex object. 9 | * @param securityAttributes Optional. A pointer to a SECURITY_ATTRIBUTES structure. 10 | */ 11 | __New(name?, initialOwner := 0, securityAttributes := 0) { 12 | if !(this.ptr := DllCall("CreateMutex", "ptr", securityAttributes, "int", !!initialOwner, "ptr", IsSet(name) ? StrPtr(name) : 0)) 13 | throw Error("Unable to create or open the mutex", -1) 14 | } 15 | /** 16 | * Tries to lock (or signal) the mutex within the timeout period. 17 | * @param timeout The timeout period in milliseconds (default is infinite wait) 18 | * @returns {Integer} 0 = successful, 0x80 = abandoned, 0x120 = timeout, 0xFFFFFFFF = failed 19 | */ 20 | Lock(timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", this, "int", timeout, "int") 21 | ; Releases the mutex (resets it back to the unsignaled state) 22 | Release() => DllCall("ReleaseMutex", "ptr", this) 23 | __Delete() => DllCall("CloseHandle", "ptr", this) 24 | } 25 | 26 | class Semaphore { 27 | /** 28 | * Creates a new semaphore or opens an existing one. The semaphore is destroyed once all handles 29 | * to it are closed. 30 | * 31 | * CreateSemaphore argument list: 32 | * @param initialCount The initial count for the semaphore object. This value must be greater 33 | * than or equal to zero and less than or equal to maximumCount. 34 | * @param maximumCount The maximum count for the semaphore object. This value must be greater than zero. 35 | * @param name Optional. The name of the semaphore object. 36 | * @param securityAttributes Optional. A pointer to a SECURITY_ATTRIBUTES structure. 37 | * @returns {Object} 38 | * 39 | * OpenSemaphore argument list: 40 | * @param name The name of the semaphore object. 41 | * @param desiredAccess Optional: The desired access right to the semaphore object. Default is 42 | * SEMAPHORE_MODIFY_STATE = 0x0002 43 | * @param inheritHandle Optional: If this value is 1, processes created by this process will inherit the handle. 44 | * @returns {Object} 45 | */ 46 | __New(initialCount, maximumCount?, name?, securityAttributes := 0) { 47 | if IsSet(initialCount) && IsSet(maximumCount) && IsInteger(initialCount) && IsInteger(maximumCount) { 48 | if !(this.ptr := DllCall("CreateSemaphore", "ptr", securityAttributes, "int", initialCount, "int", maximumCount, "ptr", IsSet(name) ? StrPtr(name) : 0)) 49 | throw Error("Unable to create the semaphore", -1) 50 | } else if IsSet(initialCount) && initialCount is String { 51 | if !(this.ptr := DllCall("OpenSemaphore", "int", maximumCount ?? 0x0002, "int", !!(name ?? 0), "ptr", IsSet(initialCount) ? StrPtr(initialCount) : 0)) 52 | throw Error("Unable to open the semaphore", -1) 53 | } else 54 | throw ValueError("Invalid parameter list!", -1) 55 | } 56 | /** 57 | * Tries to decrease the semaphore count by 1 within the timeout period. 58 | * @param timeout The timeout period in milliseconds (default is infinite wait) 59 | * @returns {Integer} 0 = successful, 0x80 = abandoned, 0x120 = timeout, 0xFFFFFFFF = failed 60 | */ 61 | Wait(timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", this, "int", timeout, "int") 62 | /** 63 | * Increases the count of the specified semaphore object by a specified amount. 64 | * @param count Optional. How much to increase the count, default is 1. 65 | * @param out Is set to the result of the DllCall 66 | * @returns {number} The previous semaphore count 67 | */ 68 | Release(count := 1, &out?) => (out := DllCall("ReleaseSemaphore", "ptr", this, "int", count, "int*", &prevCount:=0), prevCount) 69 | __Delete() => DllCall("CloseHandle", "ptr", this) 70 | } 71 | 72 | /* Waits for an object to be signaled within the timeout period. 73 | * @param timeout The timeout period in milliseconds (default is infinite wait) 74 | * @returns {Integer} 75 | * 0 = successful 76 | * 0x80 = abandoned 77 | * 0x120 = timeout 78 | * 0xFFFFFFFF = failed (A_LastError contains the error message) 79 | * @docs https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject 80 | */ 81 | WaitForSingleObject(obj, timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", obj, "int", timeout, "int") 82 | 83 | /* 84 | * Waits for multiple objects to be signaled. 85 | * @param objArray An array of object handles, or AHK objects with ptr properties 86 | * @param waitAll If 1 then waits for all objects to be signaled, if 0 then at least one. 87 | * @param timeout The timeout period in milliseconds (default is infinite wait) 88 | * @returns {Integer} 89 | * 0 = successful 90 | * 0x80 = abandoned 91 | * 0x120 = timeout 92 | * 0xFFFFFFFF = failed (A_LastError contains the error message) 93 | * @docs https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects 94 | */ 95 | WaitForMultipleObjects(objArray, waitAll:=1, timeout:=0xFFFFFFFF) { 96 | buf := Buffer(objArray.Length*A_PtrSize, 0) 97 | for i, obj in objArray 98 | NumPut("ptr", IsObject(obj) ? (obj.HasKey("ptr") ? obj.ptr : ObjPtr(obj)) : obj, buf, (i-1)*A_PtrSize) 99 | return DllCall("WaitForMultipleObjects", "int", objArray.Length, "ptr", buf, "int", !!waitAll, "int", timeout, "int") 100 | } -------------------------------------------------------------------------------- /Lib/XHotstring.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | /** 3 | * Implements the `Hotstring` function but for regular expression trigger-words and replacements. 4 | * 5 | * `XHotstring(String [, Replacement, OnOffToggle, SendFunction])` 6 | * > See documentation for `Hotstring`. Only the `K` option is currently not supported. 7 | * > SendFunction can optionally be set to use a custom function to send the text (eg via Clipboard) 8 | * 9 | * `XHotstring(NewOptions)` 10 | * 11 | * `XHotstring(SubFunction [, Value1])` 12 | * 13 | * `XHotstring.HotIf(Callback := "")` 14 | * > Sets a new HotIf context for the next registered hotstrings 15 | * 16 | * `XHotstring.Delete(Trigger)` 17 | * > Removes a XHotstring completely. This differs from OnOffToggle because OnOffToggle does 18 | * not delete the hotstring, only temporarily enables/disables it. 19 | * 20 | * `XHotstring.Start()` 21 | * > Resumes the hotstring recognizer after being stopped with `XHotstring.Stop()`. 22 | * 23 | * `XHotstring.Stop()` 24 | * > Stops the hotstring recognizer. 25 | * 26 | * `XHotstring.Reset()` 27 | * > Immediately resets the hotstring recognizer content. 28 | * 29 | * `XHotstring.HotstringRecognizer` 30 | * > Current XHotstring recognizer content. 31 | * 32 | * `XHotstring.IsActive` 33 | * > Whether the XHotstring is currently gathering input and active. 34 | * 35 | * `XHotstring.EndChars` 36 | * > List of keys that, when pressed, may trigger a XHotstring. 37 | * Default is ``-()[]{}':;`"/\,.?!`n`s`t`` 38 | * 39 | * `XHotstring.ResetKeys` 40 | * > List of keys that, when pressed, will reset the XHotstring recognizer content. 41 | * Default is `{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}` 42 | * 43 | * `XHotstring.MouseReset` 44 | * > Controls whether a mouse button click resets the recognizer or not. By default this is On. 45 | * 46 | * `XHotstring.SendFunction` 47 | * > Can be used to change the default Send function for all new XHotstrings. Eg. `XHotstring.SendFunction := SendEvent` 48 | * Default is `Send` 49 | */ 50 | class XHotstring { 51 | static HotstringRecognizer := "", IsActive := 0, __EndChars := "-()[]{}':;`"/\,.?!`n`s`t", SendFunction := Send 52 | , __DefaultOptions := Map(), __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}" 53 | , __CurrentHotIf := "", __RegisteredHotstrings := [], __HotstringsReadyToTrigger := [] 54 | , __MouseReset := 1, __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0 55 | , __ActiveEndChars := "", __ActiveNoModifierEndChars := "", __ActiveModifierEndChars := "" 56 | , __SpecialChars := "^+!#{}", __ShiftPressed := 0 57 | /** 58 | * Registers or modifies a XHotstring. See documentation for `Hotstring` for more information. 59 | * @param {String} Trigger Either a trigger string in the format `:options:trigger`, or NewOptions that 60 | * sets new options for all new XHotstrings, or SubFunction ("MouseReset", "Reset", or "EndChars") 61 | * 62 | * Options (follow with a zero to turn them off): 63 | * > `M0` (M followed by a zero): Turn off replacing RegEx back-references in the replacement word. 64 | * > `*` (asterisk): An ending character is not required to trigger the hotstring. 65 | * > `?` (question mark): The hotstring will be triggered even when it is inside another word. 66 | * NOTE: If this is *not* used then the trigger RegEx will be modified to be "(?<=\s|^)TRIGGER$". 67 | * > `B0` (B followed by a zero): Automatic backspacing is not done to erase the abbreviation you type. 68 | * > `C`: Case sensitive: When you type an abbreviation, it must exactly match the case defined in the script. 69 | * NOTE: If this is used and the RegEx trigger options does not contain `i`, then the `i` will be added. 70 | * If this is *not* used then `i` will be removed from the options. 71 | * > `C1` (C followed by a one): Turn off case conform, meaning the replacement case will not match the trigger case. 72 | * > `O`: Omit the ending character of auto-replace hotstrings when the replacement is produced. 73 | * > `T`: Send the replacement string in Text mode, meaning special characters such as `+` are typed out literally. 74 | * > `R`: Send the replacement string in Raw mode. 75 | * > `SI`, `SE`, `SP`: Use SendInput, SendEvent, or SendPlay to send the replacement string. 76 | * > `Z`: Reset the hotstring recognizer after each replacement. 77 | * 78 | * @param {String | Func} Replacement The replacement string or a callback function. The replacement string can contain RegEx 79 | * match groups (see `RegExReplace` for more information). 80 | * The callback function `Callback(TriggerMatch, EndChar, HS)` will receive a RegExMatch object 81 | * TriggerMatch, the ending character, and a XHotstring object containing information about the 82 | * XHotstring that matched. 83 | * @param {String | 'On' | 'Off' | 'Toggle'} OnOffToggle 84 | * @param {Func} SendFunction The Send function to use for sending the replacement. Default is `Send`. 85 | * @returns {Object} The hotstring object {Trigger, UnmodifiedTrigger, TriggerWithOptions, Replacement, HotIf, SendFunction, Options, Active} 86 | * Where Trigger is the RegEx trigger actually used for matching (this might be a modified version eg for case-sensitivity), 87 | * UnmodifiedTrigger is the original version, Options is a Map of all the options values (this shouldn't be modified manually). 88 | */ 89 | static Call(Trigger, Replacement?, OnOffToggle?, SendFunction?) { 90 | if !this.__RegisteredHotstrings.Length 91 | this.Start() 92 | if !RegExMatch(Trigger, "^:([^:]*):", &Options:="") { 93 | if Trigger = "MouseReset" || (Trigger := "NoMouse" && !(Replacement := 0)) 94 | return (Prev := this.MouseReset, this.MouseReset := Replacement ?? Prev, Prev) 95 | else if Trigger = "Reset" 96 | return this.Reset() 97 | else if Trigger = "EndChars" 98 | return (Prev := this.EndChars, this.EndChars := Replacement ?? Prev, Prev) 99 | 100 | return (Prev := this.__OptionsToString(this.__DefaultOptions), this.__ParseOptions(this.__DefaultOptions, Trigger), Prev) 101 | } 102 | TriggerWithOptions := Trigger, Options := Options[1], Trigger := RegExReplace(Trigger, "^:([^:]*):",,, 1) 103 | if Trigger = "" 104 | throw ValueError("Invalid XHotstring trigger", -1, "Can't be an empty string") 105 | for HS in this.__RegisteredHotstrings { 106 | if HS.TriggerWithOptions == TriggerWithOptions && HS.HotIf == this.__CurrentHotIf { 107 | if IsSet(Replacement) 108 | HS.Replacement := Replacement 109 | if IsSet(Options) 110 | this.__ParseOptions(HS.Options, Options, HS) 111 | if IsSet(OnOffToggle) 112 | HS.Active := (OnOffToggle = "On" || OnOffToggle = 1) ? 1 : (OnOffToggle = "Off" || OnOffToggle = 0) ? 0 : (OnOffToggle = "Toggle" || OnOffToggle = -1) ? !HS.Active : MsgBox("Invalid OnOffOptions", "Error") 113 | if IsSet(SendFunction) 114 | HS.SendFunction := SendFunction 115 | return HS 116 | } 117 | } 118 | if !IsSet(Replacement) 119 | throw ValueError("No such XHotstring registered", -1, Trigger) 120 | 121 | opts := this.__DefaultOptions.Clone() 122 | this.__RegisteredHotstrings.Push(HS := {TriggerWithOptions:TriggerWithOptions, Trigger:Trigger, UnmodifiedTrigger:Trigger, Replacement:Replacement, HotIf:this.__CurrentHotIf, SendFunction:SendFunction ?? this.SendFunction, Options:opts, Active:1}) 123 | this.__ParseOptions(opts, Options, HS) 124 | if HasMethod(Replacement) 125 | opts["X"] := 1 126 | return HS 127 | } 128 | ; Deletes the specified hotstring 129 | static Delete(Trigger) { 130 | for HS in this.__RegisteredHotstrings { 131 | if HS.TriggerWithOptions == Trigger && HS.HotIf == this.__CurrentHotIf { 132 | this.__RegisteredHotstrings.Delete(HS) 133 | if !this.__RegisteredHotstrings.Count 134 | this.Stop() 135 | return 136 | } 137 | } 138 | throw ValueError("No such XHotstring registered", -1, Trigger) 139 | } 140 | /** 141 | * Sets a new HotIf callback and returns the previous one 142 | * @param {Func} Callback 143 | * @returns {String | Func} 144 | */ 145 | static HotIf(Callback := "") { 146 | LastHotIf := this.__CurrentHotIf 147 | this.__CurrentHotIf := Callback 148 | return LastHotIf 149 | } 150 | ; Gets or sets the new MouseReset value 151 | static MouseReset { 152 | get => this.__MouseReset 153 | set => (this.__SetMouseReset(Value), this.__MouseReset := !!Value) 154 | } 155 | ; Gets or sets new end-characters. This affects all registered XHotstrings 156 | static EndChars { 157 | get => this.__EndChars 158 | set { 159 | static lpKeyState := Buffer(256,0), pwszBuff := Buffer(4) 160 | if !(Value is String) || Value = "" 161 | throw ValueError("Invalid EndChars", -1) 162 | this.__EndChars := Value 163 | this.__NoModifierEndChars := "" 164 | this.__ModifierEndChars := InStr(Value, " ") ? " " : "" 165 | if InStr(Value, "`n") 166 | Value .= "`r" 167 | NumPut("char", 0, lpKeyState, 0x10) 168 | Loop parse Value { 169 | if (len := DllCall("ToUnicode", "uint", VK := GetKeyVK(A_LoopField), "uint", SC := GetKeySC(A_LoopField), "ptr", lpKeyState, "ptr", pwszBuff, "int", pwszBuff.size, "uint", 0)) <= 0 170 | continue 171 | if StrGet(pwszBuff, len, "UTF-16") == A_LoopField 172 | this.__NoModifierEndChars .= "{" A_LoopField "}" 173 | } 174 | NumPut("char", 0x80, lpKeyState, 0x10) 175 | Loop parse Value { 176 | if (len := DllCall("ToUnicode", "uint", VK := GetKeyVK(A_LoopField), "uint", SC := GetKeySC(A_LoopField), "ptr", lpKeyState, "ptr", pwszBuff, "int", pwszBuff.size, "uint", 0)) <= 0 177 | continue 178 | if StrGet(pwszBuff, len, "UTF-16") == A_LoopField 179 | this.__ModifierEndChars .= "{" A_LoopField "}" 180 | } 181 | } 182 | } 183 | ; Can be used to resume the hotstring recognizer after stopping it with `XHotstring.Stop()` 184 | static Start() { 185 | if this.IsActive 186 | return 187 | this.Reset() 188 | this.MouseReset := this.MouseReset 189 | this.EndChars := this.EndChars 190 | this.__Hook.Start() 191 | this.IsActive := 1 192 | } 193 | ; Stops the hotstring recognizer 194 | static Stop() { 195 | this.IsActive := 0 196 | this.__SetMouseReset(0) 197 | this.__Hook.Stop() 198 | } 199 | ; Immediately resets the hotstring recognizer 200 | static Reset(*) => (this.__DeactivateEndChars(), this.HotstringRecognizer := "", this.__hWnd := DllCall("GetForegroundWindow", "ptr")) 201 | 202 | ; ONLY INTERNAL METHODS AHEAD 203 | 204 | static __New() { 205 | this.Prototype.__Static := this 206 | this.__Hook := InputHook("V L0 I" A_SendLevel) 207 | this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N") 208 | this.__Hook.OnKeyDown := this.__OnKeyDown.Bind(this) 209 | this.__Hook.OnKeyUp := this.__OnKeyUp.Bind(this) 210 | this.__Hook.OnChar := this.__AddChar.Bind(this) 211 | ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode 212 | this.DefineProp("MinSendLevel", { 213 | set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 214 | get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)}) 215 | this.DefineProp("ResetKeys", { 216 | set:((this, dummy, value, *) => (this.__Hook.KeyOpt(this.__ResetKeys, "-N"), this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 217 | get:((this, *) => this.__ResetKeys).Bind(this)}) 218 | this.__DefaultOptions.CaseSense := 0 219 | this.__DefaultOptions.Set("*", 0, "?", 0, "B", 1, "C", 0, "O", 0, "T", 0, "R", 0, "S", 0, "M", 1, "X", 0, "Z", 0) 220 | } 221 | static __AddChar(ih, char) { 222 | Critical 223 | hWnd := DllCall("GetForegroundWindow", "ptr") 224 | if char && InStr(this.EndChars, char) { 225 | if hWnd != this.__hWnd { 226 | if InStr(this.__ActiveEndChars, char) ; This was blocked, so resend it 227 | Send "{Blind}" char 228 | } else { 229 | for Active in this.__HotstringsReadyToTrigger { 230 | if Active.HS.HotIf = "" || Active.HS.HotIf.Call(Active.HS, Active.TriggerMatch) 231 | return this.__TriggerHS(Active.HS, Active.TriggerMatch, Active.Replacement, char) 232 | } 233 | } 234 | } 235 | this.__DeactivateEndChars() 236 | if this.__hWnd != hWnd 237 | this.__hWnd := hWnd, this.HotstringRecognizer := "" 238 | if char = "" { 239 | this.HotstringRecognizer := RegExReplace(this.HotstringRecognizer, "s)\X$",,, 1) 240 | } else { 241 | this.HotstringRecognizer .= char 242 | if StrLen(this.HotstringRecognizer) > 100 243 | this.HotstringRecognizer := SubStr(this.HotstringRecognizer, -50) 244 | } 245 | 246 | for HS in this.__RegisteredHotstrings { 247 | if HS.Active && (Pos := RegExMatch(this.HotstringRecognizer, HS.Trigger, &Match:="")) && Match[] { 248 | Replacement := HS.Options["M"] && !HS.Options["X"] ? SubStr(RegExReplace(this.HotstringRecognizer, HS.Trigger, HS.Replacement,,1), Pos) : HS.Replacement 249 | if HS.Options["*"] && (HS.HotIf = "" || HS.HotIf.Call(HS, Match)) { 250 | return this.__TriggerHS(HS, Match, Replacement, "") 251 | } else { 252 | this.__ActivateEndChars(HS, Match, Replacement) 253 | } 254 | } 255 | } 256 | } 257 | static __OnKeyDown(ih, vk, sc) { 258 | Critical 259 | if vk = 8 260 | this.__AddChar(ih, "") 261 | else { 262 | if (vk = 0xA0 || vk = 0xA1 || vk == 0x10) { ; Shift is pressed 263 | this.__ShiftPressed := 1 264 | if this.__ActiveNoModifierEndChars 265 | this.__Hook.KeyOpt(this.__ActiveNoModifierEndChars, "-S") 266 | if this.__ActiveEndChars 267 | this.__Hook.KeyOpt(this.__ActiveEndChars, "+S") 268 | } else 269 | this.Reset() 270 | } 271 | } 272 | static __OnKeyUp(ih, vk, sc) { 273 | Critical 274 | if (vk = 0xA0 || vk = 0xA1 || vk == 0x10) { ; Shift is released 275 | this.__ShiftPressed := 0 276 | if this.__ActiveEndChars 277 | this.__Hook.KeyOpt(this.__ActiveEndChars, "-S") 278 | if this.__ActiveNoModifierEndChars 279 | this.__Hook.KeyOpt(this.__ActiveNoModifierEndChars, "+S") 280 | } 281 | } 282 | static __ParseOptions(OptObj, OptStr, HS?) { 283 | Loop parse OptStr { 284 | switch A_LoopField, 0 { 285 | case "0": 286 | OptObj[last] := 0 287 | case "1": 288 | if last = "C" 289 | OptObj["C"] := 2 290 | case "E", "I", "P": 291 | if last = "S" { 292 | OptObj["S"] := A_LoopField 293 | } else 294 | throw ValueError("Invalid Option", -1, A_LoopField) 295 | case "*", "?", "B", "C", "O", "T", "R", "M", "Z": 296 | OptObj[A_LoopField] := 1 297 | case " ", "`t", "S": 298 | continue 299 | default: 300 | throw ValueError("Invalid Option", -1, A_LoopField) 301 | } 302 | last := A_LoopField 303 | } 304 | if IsSet(HS) { 305 | HS.SendFunction := OptObj["S"] = "I" ? SendInput : OptObj["S"] = "E" ? SendEvent : OptObj["S"] = "P" ? SendPlay : HS.SendFunction 306 | HS.Trigger := HS.UnmodifiedTrigger 307 | RegExOptsExist := RegExMatch(HS.Trigger, "^([^(\\]+)\)", &RegExOpts:="") 308 | if OptObj["C"] != 1 && !RegExOptsExist { 309 | HS.Trigger := "i)" HS.Trigger 310 | } 311 | if SubStr(HS.Trigger, 1, 1) = ")" 312 | HS.Trigger := SubStr(HS.Trigger, 2) 313 | if !OptObj["?"] 314 | HS.Trigger := RegExReplace(HS.Trigger, "^([^(\\]+\))?", "$1(?<=\s|^)",, 1) "$" 315 | } 316 | } 317 | static __OptionsToString(OptObj) { 318 | OptStr := "" 319 | for k, v in OptObj { 320 | if k == "X" 321 | continue 322 | else if k == "C" 323 | OptStr .= k (v == 2 ? "1" : v == 1 ? "" : "0") 324 | else 325 | OptStr .= k (v == 1 ? "" : v) 326 | } 327 | return OptStr 328 | } 329 | static __ActivateEndChars(HS, TriggerMatch, Replacement) { 330 | static lpKeyState := Buffer(256, 0) 331 | if this.__ActiveEndChars 332 | return 333 | this.__HotstringsReadyToTrigger.Push({HS:HS, TriggerMatch:TriggerMatch, Replacement:Replacement}) 334 | this.__ActiveEndChars := this.__EndChars 335 | this.__ActiveNoModifierEndChars := this.__NoModifierEndChars 336 | this.__ActiveModifierEndChars := this.__ModifierEndChars 337 | ShiftState := GetKeyState("Shift") 338 | if this.__ActiveModifierEndChars { 339 | if ShiftState 340 | this.__Hook.KeyOpt(this.__ActiveModifierEndChars, "+S") 341 | this.__Hook.KeyOpt("{Shift}{LShift}{RShift}", "+N") 342 | } 343 | if this.__ActiveNoModifierEndChars && !ShiftState 344 | this.__Hook.KeyOpt(this.__ActiveNoModifierEndChars, "+S") 345 | } 346 | static __DeactivateEndChars() { 347 | if this.__ActiveEndChars = "" 348 | return 349 | this.__HotstringsReadyToTrigger := [] 350 | if this.__ActiveNoModifierEndChars 351 | this.__Hook.KeyOpt(this.__ActiveNoModifierEndChars, "-S") 352 | if this.__ActiveModifierEndChars 353 | this.__Hook.KeyOpt(this.__ActiveModifierEndChars, "-S") 354 | this.__Hook.KeyOpt("{Shift}{LShift}{RShift}", "-N") 355 | this.__ActiveEndChars := "" 356 | } 357 | static __TriggerHS(HS, TriggerMatch, Replacement, EndChar, *) { 358 | Critical 359 | local opts := HS.Options, TriggerText := TriggerMatch[], BS := 0, B := opts["B"] 360 | this.__DeactivateEndChars() 361 | 362 | if opts["X"] { 363 | replacement := "", TextMode := "" 364 | if B 365 | RegExReplace(TriggerText, "s)\X",, &BS) 366 | } else { 367 | if (opts["C"] < 2) && ((ThisHotstringLetters := RegexReplace(TriggerText, "\P{L}")) != "") && IsUpper(SubStr(ThisHotstringLetters, 1, 1), 'Locale') { 368 | if IsUpper(trail := SubStr(ThisHotstringLetters, 2), 'Locale') 369 | replacement := StrUpper(replacement) 370 | else 371 | replacement := StrUpper(SubStr(replacement, 1, 1)) (IsLower(trail, 'Locale') ? StrLower(SubStr(replacement, 2)) : replacement) 372 | } 373 | TextMode := opts["T"] ? "{Text}" : opts["R"] ? "{Raw}" : "" 374 | if B { 375 | RegExReplace(TriggerText, "s)\X",, &MaxBS:=0) 376 | BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1}) 377 | RegExMatch(TriggerText, "s)(?:\X)(?CBoundGraphemeCallout)") 378 | if !TextMode && info.GraphemeLength && (SpecialChar := RegExMatch(TriggerText, "[\Q" this.__SpecialChars "\E]")) && SpecialChar < info.Pos { 379 | RegExReplace(SubStr(TriggerText, 1, SpecialChar), "s)\X",, &Diff:=0) 380 | BS := MaxBS - Diff + 1, replacement := SubStr(replacement, SpecialChar) 381 | } else { 382 | BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos) 383 | } 384 | } 385 | } 386 | 387 | Omit := opts["O"] || (opts["X"] && B) 388 | 389 | if (str := (B ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar))) 390 | HS.SendFunction.Call(str) 391 | 392 | if B || opts["Z"] 393 | this.HotstringRecognizer := "" 394 | if !Omit 395 | this.HotstringRecognizer .= EndChar 396 | 397 | Critical 'Off' 398 | if opts["X"] 399 | HS.Replacement.Call(TriggerMatch, EndChar, HS) 400 | 401 | GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1 402 | } 403 | 404 | static __SetMouseReset(NewValue) { 405 | static MouseRIProc := this.__MouseRawInputProc.Bind(this), DevSize := 8 + A_PtrSize, RIDEV_INPUTSINK := 0x00000100 406 | , RIDEV_REMOVE := 0x00000001, RAWINPUTDEVICE := Buffer(DevSize, 0), Active := 0, g := Gui() 407 | if !!NewValue = Active 408 | return 409 | if Active := !!NewValue { 410 | ; Register mouse for WM_INPUT messages. 411 | NumPut("UShort", 1, "UShort", 2, "Uint", RIDEV_INPUTSINK, "Ptr", g.hWnd, RAWINPUTDEVICE) 412 | DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize) 413 | OnMessage(0x00FF, MouseRIProc) 414 | } else { 415 | OnMessage(0x00FF, MouseRIProc, 0) 416 | NumPut("Uint", RIDEV_REMOVE, RAWINPUTDEVICE, 4) 417 | DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize) 418 | } 419 | } 420 | 421 | static __MouseRawInputProc(wParam, lParam, *) { 422 | ; RawInput statics 423 | static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, pcbSize:=8+2*A_PtrSize, offsets := {usButtonFlags: (12+2*A_PtrSize), x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput 424 | ; Get hDevice from RAWINPUTHEADER to identify which mouse this data came from 425 | header := Buffer(pcbSize, 0) 426 | If !DllCall("GetRawInputData", "UPtr", lParam, "uint", 0x10000005, "Ptr", header, "Uint*", &pcbSize, "Uint", pcbSize) 427 | return 0 428 | ThisMouse := NumGet(header, 8, "UPtr") 429 | ; Find size of rawinput data - only needs to be run the first time. 430 | if (!iSize) { 431 | r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", &iSize, "UInt", 8 + (A_PtrSize * 2)) 432 | uRawInput := Buffer(iSize, 0) 433 | } 434 | ; Get RawInput data 435 | r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", uRawInput, "UInt*", &sz := iSize, "UInt", 8 + (A_PtrSize * 2)) 436 | Loop { 437 | usButtonFlags := NumGet(uRawInput, offsets.usButtonFlags, "ushort") 438 | if usButtonFlags & 0x0001 || usButtonFlags & 0x0004 || usButtonFlags & 0x0010 || usButtonFlags & 0x0040 || usButtonFlags & 0x0100 { 439 | this.__DeactivateEndChars(), this.HotstringRecognizer := "", MouseGetPos(,, &hWnd), this.__hWnd := hWnd 440 | While DllCall("GetRawInputBuffer", "Ptr", uRawInput, "Uint*", &sz := iSize, "UInt", 8 + (A_PtrSize * 2)) 441 | continue 442 | } 443 | } Until !DllCall("GetRawInputBuffer", "Ptr", uRawInput, "Uint*", &sz := iSize, "UInt", 8 + (A_PtrSize * 2)) 444 | } 445 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AHK-v2-libraries 2 | # Useful libraries for AHK v2 3 | 4 | ## Misc.ahk 5 | Implements useful miscellaneous functions 6 | 7 | ### Range 8 | Allows looping from start to end with step. 9 | ``` 10 | Range(stop) 11 | Range(start, stop [, step ]) 12 | ``` 13 | Usage: `for v in Range(2,10)` -> loops from 2 to 10 14 | `for v in Range(10,1,-1)` -> loops from 10 to 1 backwards 15 | 16 | ### RegExMatchAll 17 | Returns all RegExMatch results for NeedleRegEx in Haystack in an array: [RegExMatchInfo1, RegExMatchInfo2, ...] 18 | ``` 19 | RegExMatchAll(Haystack, NeedleRegEx, StartingPosition := 1) 20 | ``` 21 | 22 | ### Swap 23 | Swaps the values of two variables 24 | ``` 25 | Swap(&a, &b) 26 | ``` 27 | 28 | ### Print 29 | Prints the formatted value of a variable (number, string, array, map, object) 30 | ``` 31 | Print(value?, func?, newline?) 32 | ``` 33 | 34 | ## String.ahk 35 | Implements useful string functions and lets strings be treated as objects 36 | 37 | Native AHK functions as methods: 38 | ``` 39 | String.ToUpper() 40 | .ToLower() 41 | .ToTitle() 42 | .Split([Delimiters, OmitChars, MaxParts]) 43 | .Replace(Needle [, ReplaceText, CaseSense, &OutputVarCount, Limit]) 44 | .Trim([OmitChars]) 45 | .LTrim([OmitChars]) 46 | .RTrim([OmitChars]) 47 | .Compare(comparison [, CaseSense]) 48 | .Sort([, Options, Function]) 49 | .Find(Needle [, CaseSense, StartingPos, Occurrence]) 50 | .SplitPath() => returns object with keys FileName, Dir, Ext, NameNoExt, Drive 51 | .RegExMatch(needleRegex, &match?, startingPos?) 52 | .RegExReplace(needle, replacement?, &count?, limit?, startingPos?) 53 | ``` 54 | 55 | ``` 56 | String[n] => gets nth character 57 | String[i,j] => substring from i to j 58 | String.Length 59 | String.Count(searchFor) 60 | String.Insert(insert, into [, pos]) 61 | String.Delete(string [, start, length]) 62 | String.Overwrite(overwrite, into [, pos]) 63 | String.Repeat(count) 64 | Delimeter.Concat(words*) 65 | 66 | String.LineWrap([column:=56, indentChar:=""]) 67 | String.WordWrap([column:=56, indentChar:=""]) 68 | String.ReadLine(line [, delim:="`n", exclude:="`r"]) 69 | String.DeleteLine(line [, delim:="`n", exclude:="`r"]) 70 | String.InsertLine(insert, into, line [, delim:="`n", exclude:="`r"]) 71 | 72 | String.Reverse() 73 | String.Contains(needle1 [, needle2, needle3...]) 74 | String.RemoveDuplicates([delim:="`n"]) 75 | String.LPad(count) 76 | String.RPad(count) 77 | 78 | String.Center([fill:=" ", symFill:=0, delim:="`n", exclude:="`r", width]) 79 | String.Right([fill:=" ", delim:="`n", exclude:="`r"]) 80 | ``` 81 | 82 | ## Array.ahk 83 | Implements useful array functions 84 | 85 | ``` 86 | Array.Slice(start:=1, end:=0, step:=1) => Returns a section of the array from 'start' to 'end', 87 | optionally skipping elements with 'step'. 88 | Array.Swap(a, b) => Swaps elements at indexes a and b. 89 | Array.Map(func) => Applies a function to each element in the array. 90 | Array.Filter(func) => Keeps only values that satisfy the provided function 91 | Array.Reduce(func, initialValue?) => Applies a function cumulatively to all the values in 92 | the array, with an optional initial value. 93 | Array.IndexOf(value, start:=1) => Finds a value in the array and returns its index. 94 | Array.Find(func, &match?, start:=1) => Finds a value satisfying the provided function and 95 | returns the index. match will be set to the found value. 96 | Array.Reverse() => Reverses the array. 97 | Array.Count(value) => Counts the number of occurrences of a value. 98 | Array.Sort(Key?, Options?, Callback?) => Sorts an array, optionally by object values. 99 | Array.Join(delim:=",") => Joins all the elements to a string using the provided delimiter. 100 | Array.Shuffle() => Randomizes the array. 101 | Array.Flat() => Turns a nested array into a one-level array. 102 | Array.Extend(arr) => Adds the contents of another array to the end of this one. 103 | ``` 104 | 105 | ## Acc.ahk 106 | 107 | Accessibility library for AHK v2 108 | 109 | Acc.ahk examples are available at the Acc v2 GitHub: https://github.com/Descolada/Acc-v2 110 | 111 | Short introduction: 112 | Acc v2 in not a port of v1, but instead a complete redesign to incorporate more object-oriented approaches. 113 | 114 | Notable changes: 115 | 1) All Acc elements are now array-like objects, where the "Length" property contains the number of children, any nth children can be accessed with element[n], and children can be iterated over with for loops. 116 | 2) Acc main functions are contained in the global Acc object 117 | 3) Element methods are contained inside element objects 118 | 4) Element properties can be used without the "acc" prefix 119 | 5) ChildIds have been removed (are handled in the backend), but can be accessed through 120 | el.ChildId 121 | 6) Additional methods have been added for elements, such as FindFirst, FindAll, Click 122 | 7) Acc constants are included in the Acc object 123 | 8) AccViewer is built into the library: when ran directly the AccViewer will show, when included 124 | in another script then it won't show (but can be accessed by calling Acc.Viewer()) 125 | 126 | Acc constants/properties: 127 | ``` 128 | Constants can be accessed as properties (eg Acc.OBJID.CARET), or the property name can be 129 | accessed by getting as an item (eg Acc.OBJID[0xFFFFFFF8]) 130 | 131 | OBJID 132 | STATE 133 | ROLE 134 | NAVDIR 135 | SELECTIONFLAG 136 | EVENT 137 | 138 | Explanations for the constants are available in Microsoft documentation: 139 | https://docs.microsoft.com/en-us/windows/win32/winauto/constants-and-enumerated-types 140 | ``` 141 | 142 | Acc methods: 143 | ``` 144 | ObjectFromPoint(x:=unset, y:=unset, &idChild := "", activateChromium := True) 145 | Gets an Acc element from screen coordinates X and Y (NOT relative to the active window). 146 | ObjectFromWindow(hWnd:="A", idObject := 0, activateChromium := True) 147 | Gets an Acc element from a WinTitle, by default the active window. 148 | Additionally idObject can be specified from Acc.OBJID constants (eg to get the Caret location). 149 | GetRootElement() 150 | Gets the Acc element for the Desktop 151 | ActivateChromiumAccessibility(hWnd) 152 | Sends the WM_GETOBJECT message to the Chromium document element and waits for the 153 | app to be accessible to Acc. This is called when ObjectFromPoint or ObjectFromWindow 154 | activateChromium flag is set to True. A small performance increase may be gotten 155 | if that flag is set to False when it is not needed. 156 | RegisterWinEvent(event, callback) 157 | Registers an event from Acc.EVENT to a callback function and returns a new object 158 | containing the WinEventHook 159 | The callback function needs to have three arguments: 160 | CallbackFunction(oAcc, Event, EventTime) 161 | Unhooking of the event handler will happen once the returned object is destroyed 162 | (either when overwritten by a constant, or when the script closes). 163 | 164 | Legacy methods: 165 | SetWinEventHook(eventMin, eventMax, pCallback) 166 | UnhookWinEvent(hHook) 167 | ObjectFromPath(ChildPath, hWnd:="A") => Same as ObjectFromWindow[comma-separated path] 168 | GetRoleText(nRole) => Same as element.RoleText 169 | GetStateText(nState) => Same as element.StateText 170 | Query(pAcc) => For internal use 171 | ``` 172 | 173 | IAccessible element properties: 174 | ``` 175 | Element[n] => Gets the nth element. Multiple of these can be used like a path: 176 | Element[4,1,4] will select 4th childs 1st childs 4th child 177 | Conditions (see ValidateCondition) are supported: 178 | Element[4,{Name:"Something"}] will select the fourth childs first child matching the name "Something" 179 | Conditions also accept an index (or i) parameter to select from multiple similar elements 180 | Element[{Name:"Something", i:3}] selects the third element of elements with name "Something" 181 | Negative index will select from the last element 182 | Element[{Name:"Something", i:-1}] selects the last element of elements with name "Something" 183 | Since index/i needs to be a key-value pair, then to use it with an "or" condition 184 | it must be inside an object ("and" condition), for example with key "or": 185 | Element[{or:[{Name:"Something"},{Name:"Something else"}], i:2}] 186 | Name => Gets or sets the name. All objects support getting this property. 187 | Value => Gets or sets the value. Not all objects have a value. 188 | Role => Gets the Role of the specified object in integer form. All objects support this property. 189 | RoleText => Role converted into text form. All objects support this property. 190 | Help => Retrieves the Help property string of an object. Not all objects support this property. 191 | KeyboardShortcut => Retrieves the specified object's shortcut key or access key. Not all objects support this property. 192 | State => Retrieves the current state in integer form. All objects support this property. 193 | StateText => State converted into text form 194 | Description => Retrieves a string that describes the visual appearance of the specified object. Not all objects have a description. 195 | DefaultAction => Retrieves a string that indicates the object's default action. Not all objects have a default action. 196 | Focus => Returns the focused child element (or itself). 197 | If no child is focused, an error is thrown 198 | Selection => Retrieves the selected children of this object. All objects that support selection must support this property. 199 | Parent => Returns the parent element. All objects support this property. 200 | IsChild => Checks whether the current element is of child type 201 | Length => Returns the number of children the element has 202 | Location => Returns the object's current screen location in an object {x,y,w,h} 203 | Children => Returns all children as an array (usually not required) 204 | Exists => Checks whether the element is still alive and accessible 205 | ControlID => ID (hwnd) of the control associated with the element 206 | WinID => ID (hwnd) of the window the element belongs to 207 | oAcc => ComObject of the underlying IAccessible 208 | childId => childId of the underlying IAccessible 209 | ``` 210 | 211 | IAccessible element methods: 212 | ``` 213 | Select(flags) 214 | Modifies the selection or moves the keyboard focus of the specified object. 215 | flags can be any of the SELECTIONFLAG constants 216 | DoDefaultAction() 217 | Performs the specified object's default action. Not all objects have a default action. 218 | GetNthChild(n) 219 | This is equal to element[n] 220 | GetLocation(relativeTo:="") 221 | Returns an object containing the x, y coordinates and width and height: {x:x coordinate, y:y coordinate, w:width, h:height}. 222 | relativeTo can be client, window or screen, default is A_CoordModeMouse. 223 | IsEqual(oCompare) 224 | Checks whether the element is equal to another element (oCompare) 225 | FindFirst(condition, scope:=4) 226 | Finds the first element matching the condition (see description under ValidateCondition) 227 | Scope is the search scope: 1=element itself; 2=direct children; 4=descendants (including children of children) 228 | The scope is additive: 3=element itself and direct children. 229 | The returned element also has the "Path" property with the found elements path 230 | 231 | FindFirst conditions also accept an index (or i) parameter to search for i-th element: 232 | FindFirst({Name:"Something", i:3}) finds the third element with name "Something" 233 | Negative index reverses the search direction: 234 | FindFirst({Name:"Something", i:-1}) finds the last element with name "Something" 235 | Since index/i needs to be a key-value pair, then to use it with an "or" condition 236 | it must be inside an object ("and" condition), for example with key "or": 237 | FindFirst({or:[{Name:"Something"}, {Name:"Something else"}], index:2}) 238 | FindAll(condition, scope:=4) 239 | Returns an array of elements matching the condition (see description under ValidateCondition) 240 | The returned elements also have the "Path" property with the found elements path 241 | WaitElementExist(conditionOrPath, scope:=4, timeOut:=-1) 242 | Waits an element exist that matches a condition or a path. 243 | Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds 244 | A timeout throws an error, otherwise the matching element is returned. 245 | Normalize(condition) 246 | Checks whether the current element or any of its ancestors match the condition, 247 | and returns that element. If no element is found, an error is thrown. 248 | ValidateCondition(condition) 249 | Checks whether the element matches a provided condition. 250 | Everything inside {} is an "and" condition, or a singular condition with options 251 | Everything inside [] is an "or" condition 252 | "not" key creates a not condition 253 | "matchmode" key (short form: "mm") defines the MatchMode: 1=must start with; 2=can contain anywhere in string; 3=exact match; RegEx 254 | "casesensitive" key (short form: "cs") defines case sensitivity: True=case sensitive; False=case insensitive 255 | Any other key (but usually "or") can be used to use "or" condition inside "and" condition. 256 | 257 | {Name:"Something"} => Name must match "Something" (case sensitive) 258 | {Name:"Something", matchmode:2, casesensitive:False} => Name must contain "Something" anywhere inside the Name, case insensitive 259 | {Name:"Something", RoleText:"something else"} => Name must match "Something" and RoleText must match "something else" 260 | [{Name:"Something", Role:42}, {Name:"Something2", RoleText:"something else"}] => Name=="Something" and Role==42 OR Name=="Something2" and RoleText=="something else" 261 | {Name:"Something", not:[{RoleText:"something", mm:2}, {RoleText:"something else", cs:1}]} => Name must match "something" and RoleText cannot match "something" (with matchmode=2) nor "something else" (casesensitive matching) 262 | {or:[{Name:"Something"},{Name:"Something else"}], or2:[{Role:20},{Role:42}]} 263 | Dump(scope:=1) 264 | Outputs relevant information about the element (Name, Value, Location etc) 265 | Scope is the search scope: 1=element itself; 2=direct children; 4=descendants (including children of children); 7=whole subtree (including element) 266 | The scope is additive: 3=element itself and direct children. 267 | DumpAll() 268 | Outputs relevant information about the element and all descendants of the element. This is equivalent to Dump(7) 269 | Highlight(showTime:=unset, color:="Red", d:=2) 270 | Highlights the element for a chosen period of time 271 | Possible showTime values: 272 | Unset: removes the highlighting 273 | 0: Indefinite highlighting 274 | Positive integer (eg 2000): will highlight and pause for the specified amount of time in ms 275 | Negative integer: will highlight for the specified amount of time in ms, but script execution will continue 276 | color can be any of the Color names or RGB values 277 | d sets the border width 278 | Click(WhichButton:="left", ClickCount:=1, DownOrUp:="", Relative:="", NoActivate:=False) 279 | Click the center of the element. 280 | If WhichButton is a number, then Sleep will be called with that number. 281 | Eg Click(200) will sleep 200ms after clicking 282 | If ClickCount is a number >=10, then Sleep will be called with that number. To click 10+ times and sleep after, specify "ClickCount SleepTime". Ex: Click("left", 200) will sleep 200ms after clicking. 283 | Ex: Click("left", "20 200") will left-click 20 times and then sleep 200ms. 284 | If Relative is "Rel" or "Relative" then X and Y coordinates are treated as offsets from the current mouse position. Otherwise it expects offset values for both X and Y (eg "-5 10" would offset X by -5 and Y by +10). 285 | NoActivate will cause the window not to be brought to focus before clicking if the clickable point is not visible on the screen. 286 | ControlClick(WhichButton:="left", ClickCount:=1, Options:="") 287 | ControlClicks the element after getting relative coordinates with GetLocation("client"). 288 | If WhichButton is a number, then a Sleep will be called afterwards. Ex: ControlClick(200) will sleep 200ms after clicking. Same for ControlClick("ahk_id 12345", 200) 289 | Navigate(navDir) 290 | Navigates in one of the directions specified by Acc.NAVDIR constants. Not all elements implement this method. 291 | HitTest(x, y) 292 | Retrieves the child element or child object that is displayed at a specific point on the screen. 293 | This shouldn't be used, since Acc.ObjectFromPoint uses this internally 294 | ``` 295 | -------------------------------------------------------------------------------- /Resources/DPI_Tutorial/CalculatorSevenButtonLocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Descolada/AHK-v2-libraries/599c75d30dca007c66bf13cbca390c2b7f73a6ec/Resources/DPI_Tutorial/CalculatorSevenButtonLocation.png -------------------------------------------------------------------------------- /Resources/DPI_Tutorial/CalculatorSevenButtonNoDpiAdjust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Descolada/AHK-v2-libraries/599c75d30dca007c66bf13cbca390c2b7f73a6ec/Resources/DPI_Tutorial/CalculatorSevenButtonNoDpiAdjust.png -------------------------------------------------------------------------------- /Resources/DPI_Tutorial/Calculator_icon_225%.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Descolada/AHK-v2-libraries/599c75d30dca007c66bf13cbca390c2b7f73a6ec/Resources/DPI_Tutorial/Calculator_icon_225%.PNG -------------------------------------------------------------------------------- /Resources/DPI_Tutorial/Chrome_Reload_100%.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Descolada/AHK-v2-libraries/599c75d30dca007c66bf13cbca390c2b7f73a6ec/Resources/DPI_Tutorial/Chrome_Reload_100%.PNG -------------------------------------------------------------------------------- /Resources/DPI_Tutorial/MouseMove00WithoutDpi_resized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Descolada/AHK-v2-libraries/599c75d30dca007c66bf13cbca390c2b7f73a6ec/Resources/DPI_Tutorial/MouseMove00WithoutDpi_resized.png -------------------------------------------------------------------------------- /Test/RunTests.ahk: -------------------------------------------------------------------------------- 1 | #include ..\Lib\DUnit.ahk 2 | #include Test_String.ahk 3 | #include Test_Array.ahk 4 | #include Test_Misc.ahk 5 | #include Test_Map.ahk 6 | #include Test_DPI.ahk 7 | 8 | ;DUnit("C", DPITestSuite) 9 | DUnit("C", StringTestSuite, ArrayTestSuite, MapTestSuite, MiscTestSuite) -------------------------------------------------------------------------------- /Test/Test_Acc.ahk: -------------------------------------------------------------------------------- 1 | #include "..\Lib\Misc.ahk" 2 | #include "..\Lib\Acc.ahk" 3 | #Include "..\Lib\DUnit.ahk" 4 | DUnit("C", AccTestSuite) 5 | 6 | class AccTestSuite { 7 | static Fail() { 8 | throw Error() 9 | } 10 | Begin() { 11 | Run "notepad.exe" 12 | WinWaitActive "ahk_exe notepad.exe" 13 | WinMove(0,0,1530,876) 14 | this.oAcc := Acc.ElementFromHandle("ahk_exe notepad.exe") 15 | A_Clipboard := this.oAcc.DumpAll() 16 | } 17 | End() { 18 | WinClose "ahk_exe notepad.exe" 19 | } 20 | Test_Item() { 21 | DUnit.Equal(this.oAcc.Dump(), "RoleText: window Role: 9 [Location: {x:0,y:0,w:1530,h:876}] [Name: Untitled - Notepad] [Value: ] [StateText: focusable] [State: 1441792] [Help: N/A]") 22 | DUnit.Equal(this.oAcc[4,2,4,3].Dump(), "RoleText: text Role: 41 [Location: {x:1086,y:831,w:73,h:34}] [Name: 100%] [Value: ] [StateText: normal] [KeyboardShortcut: N/A] ChildId: 3", "Item only by indexes") 23 | DUnit.Equal(this.oAcc[4,2,"menu bar"].Dump(), "RoleText: menu bar Role: 2 [Location: {x:0,y:0,w:0,h:0}] [Name: System] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: N/A] [Description: Contains commands to manipulate the window]", "Item by RoleText 1") 24 | DUnit.Equal(this.oAcc[4,2,"menu bar 2"].Dump(), "RoleText: menu bar Role: 2 [Location: {x:0,y:0,w:0,h:0}] [Name: Application] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: N/A] [Description: Contains commands to manipulate the current view or document]", "Item by RoleText and index") 25 | DUnit.Equal(this.oAcc[4,2,"status bar",3].Dump(), "RoleText: text Role: 41 [Location: {x:1086,y:831,w:73,h:34}] [Name: 100%] [Value: ] [StateText: normal] [KeyboardShortcut: N/A] ChildId: 3", "Item by RoleText 2") 26 | DUnit.Equal(this.oAcc["4.2.status bar.3"].Dump(), "RoleText: text Role: 41 [Location: {x:1086,y:831,w:73,h:34}] [Name: 100%] [Value: ] [StateText: normal] [KeyboardShortcut: N/A] ChildId: 3", "Item by string indexes and RoleText") 27 | DUnit.Equal(this.oAcc[4,-1].Dump(), this.oAcc[4,2].Dump()) 28 | DUnit.Equal(this.oAcc[4,{Name:"Status Bar"}].Dump(), this.oAcc[4,2].Dump()) 29 | DUnit.NotEqual(this.oAcc[4,{Name:"Text Editor"}].Dump(), this.oAcc[4,2].Dump()) 30 | DUnit.Equal(this.oAcc[4,-1,-4,-3].Dump(), "RoleText: text Role: 41 [Location: {x:1086,y:831,w:73,h:34}] [Name: 100%] [Value: ] [StateText: normal] [KeyboardShortcut: N/A] ChildId: 3") 31 | oEdit := this.oAcc[4,1,4] 32 | DUnit.Equal(this.oAcc[4,1].Dump(), oEdit["p2,1"].Dump()) 33 | } 34 | Test_ControlID_WinID() { 35 | oEdit := this.oAcc[4,1,4] 36 | DUnit.Equal(ControlGetHwnd("Edit1"), oEdit.ControlID) 37 | DUnit.Equal(WinExist(), oEdit.WinID) 38 | } 39 | Test_DoDefaultAction_WaitElementExist_WaitNotExist() { 40 | this.oAcc.FindElement({Name:"File"}).DoDefaultAction() 41 | oSave := this.oAcc.WaitElementExist({Name:"Save As... Ctrl+Shift+S"},1000) 42 | DUnit.True(oSave) 43 | DUnit.Equal(oSave.Dump(), "RoleText: menu item Role: 12 [Location: {x:14,y:201,w:310,h:31}] [Name: Save As... Ctrl+Shift+S] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: a] ChildId: 5") 44 | Send "{Esc}" 45 | DUnit.True(oSave.WaitNotExist()) 46 | DUnit.False(this.oAcc.WaitElementExist({Name:"Save As... Ctrl+Shift+S"}, 100)) 47 | } 48 | Test_Children() { 49 | oChildren := this.oAcc.Children 50 | DUnit.Equal(oChildren.length, 7) 51 | DUnit.Equal(oChildren[7].Dump(), "RoleText: grip Role: 4 [Location: {x:0,y:0,w:0,h:0}] [Name: ] [Value: ] [StateText: invisible] [State: 32769]") 52 | } 53 | Test_GetPath_ControlClick() { 54 | DUnit.Equal(this.oAcc.GetPath(this.oAcc[4,2,4,3]), "4,2,4,3") 55 | Run "chrome.exe --incognito autohotkey.com" 56 | WinWaitActive("ahk_exe chrome.exe") 57 | Sleep 500 58 | oChrome := Acc.ElementFromHandle("ahk_exe chrome.exe") 59 | DUnit.Equal(oChrome.GetPath(oEl := oChrome.WaitElementExist("4,1,1,2,2,2,2,1,1,1,1,1")), "4,1,1,2,2,2,2,1,1,1,1,1") 60 | docEl := oEl.Normalize({RoleText:"document", not:{Value:""}}) 61 | DUnit.Equal(docEl.Value, "https://www.autohotkey.com/") 62 | oEl.ControlClick() 63 | DUnit.True(oEl.WaitNotExist()) 64 | WinClose("ahk_exe chrome.exe") 65 | } 66 | Test_GetLocation() { 67 | WinMove(10, 10) 68 | oEl := this.oAcc[4,2,4,3] 69 | DUnit.Equal(oEl.GetLocation("screen"), {h:34, w:73, x:1096, y:841}) 70 | DUnit.Equal(oEl.GetLocation("client"), {h:34, w:73, x:1075, y:756}) 71 | DUnit.Equal(oEl.GetLocation("window"), {h:34, w:73, x:1086, y:831}) 72 | DUnit.Equal(oEl.GetLocation(""), {h:34, w:73, x:1075, y:756}) 73 | DUnit.Equal(A_CoordmodeMouse, "Client") 74 | } 75 | Test_FindElement_FindElements_ValidateCondition() { 76 | oEdit := this.oAcc[4,1,4] 77 | DUnit.Equal(this.oAcc.FindElement({Name:"Maximize", Role:43}).Dump(), "RoleText: push button Role: 43 [Location: {x:1379,y:1,w:70,h:44}] [Name: Maximize] [Value: ] [StateText: normal] [DefaultAction: Press] [Description: Makes the window full screen] ChildId: 3") 78 | DUnit.Equal(DUnit.Print(this.oAcc.FindElements([{Name:"Minimize"}, {Name:"Maximize"}])), "[Acc.IAccessible('RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: n] " 79 | . "ChildId: 4'), Acc.IAccessible('RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Maximize] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: x] ChildId: 5'), Acc.IAccessible('RoleText: push button Role:" 80 | . " 43 [Location: {x:1308,y:1,w:71,h:44}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:1379,y:1,w:70,h:44}] [Name: " 81 | . "Maximize] [Value: ] [StateText: normal] [DefaultAction: Press] [Description: Makes the window full screen] ChildId: 3'), Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768]" 82 | . " [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Maximize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: " 83 | . "Makes the window full screen] ChildId: 3'), Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), " 84 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Maximize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Makes the window full screen] ChildId: 3')]") 85 | DUnit.Equal(DUnit.Print(this.oAcc.FindElements([{Name:"Minimize"}])), "[Acc.IAccessible('RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: n] ChildId: 4'), " 86 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:1308,y:1,w:71,h:44}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), " 87 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), " 88 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2')]") 89 | DUnit.Equal(DUnit.Print(this.oAcc.FindElements([{Name:"Minimize", not:{Role:43}}])), "[Acc.IAccessible('RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: n] ChildId: 4')]") 90 | DUnit.Equal(DUnit.Print(this.oAcc.FindElements([{Name:"minim", cs:false, mm:2}])), "[Acc.IAccessible('RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: n] ChildId: 4'), " 91 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:1308,y:1,w:71,h:44}] [Name: Minimize] [Value: ] [StateText: normal] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), " 92 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2'), " 93 | . "Acc.IAccessible('RoleText: push button Role: 43 [Location: {x:0,y:0,w:0,h:0}] [Name: Minimize] [Value: ] [StateText: invisible] [State: 32768] [DefaultAction: Press] [Description: Moves the window out of the way] ChildId: 2')]") 94 | DUnit.Equal(this.oAcc.FindElement({Role:9, index:-1}).Dump(), "RoleText: window Role: 9 [Location: {x:11,y:829,w:1508,h:36}] [Name: Status Bar] [Value: ] [StateText: focusable] [State: 1048576] [Help: N/A]") 95 | DUnit.Equal(this.oAcc.FindElement({Role:9, index:2}).Dump(), "RoleText: window Role: 9 [Location: {x:11,y:829,w:1508,h:36}] [Name: Status Bar] [Value: ] [StateText: focusable] [State: 1048576] [Help: N/A]") 96 | DUnit.Equal(this.oAcc.FindElement({or:[{Name:"Save Ctrl+S"}, {Name:"Save As... Ctrl+Shift+S"}], index:-1}).Dump(), "RoleText: menu item Role: 12 [Location: {x:0,y:0,w:0,h:0}] [Name: Save As... Ctrl+Shift+S] [Value: ] [StateText: normal] [DefaultAction: Execute] [Description: N/A] [KeyboardShortcut: a] ChildId: 5") 97 | DUnit.Equal(this.oAcc.FindElement({IsEqual:oEdit}).Dump(), "RoleText: editable text Role: 42 [Location: {x:11,y:75,w:1482,h:754}] [Name: Text Editor] [Value: ] [StateText: focusable] [State: 1048580]") 98 | DUnit.Equal(this.oAcc.FindElement({Location:{w:1482,h:754}}).Dump(), "RoleText: editable text Role: 42 [Location: {x:11,y:75,w:1482,h:754}] [Name: Text Editor] [Value: ] [StateText: focusable] [State: 1048580]") 99 | DUnit.Equal(this.oAcc.FindElement([{Role:9}, {Role:10}], 3,,1).Dump(), "RoleText: client Role: 10 [Location: {x:11,y:75,w:1508,h:790}] [Name: Untitled - Notepad] [Value: ] [StateText: focusable] [State: 1048576]") 100 | DUnit.Equal(this.oAcc.FindElement({Role:9}, 2,,2), "") 101 | DUnit.Equal(this.oAcc.FindElements([{Role:11}, {Role:12}],, 2).Length, 12) 102 | DUnit.False(this.oAcc.FindElement([{Role:9}, {Role:12}],,,,1)) 103 | DUnit.True(this.oAcc.FindElement([{Role:9}, {Role:12}],,,,2)) 104 | DUnit.False(this.oAcc.FindElement([{Role:9}, {Role:12}],,,3,1)) 105 | DUnit.True(this.oAcc.FindElement([{Role:9}, {Role:12}],,,3,2)) 106 | } 107 | Test_Highlight() { 108 | (oEl := this.oAcc[4,1,4]).Highlight(0) 109 | res := MsgBox("Confirm the Highlight is visible",, 4) 110 | oEl := "" 111 | Acc.ClearHighlights() 112 | if res = "No" 113 | throw Error("Highlighting failed!") 114 | } 115 | Test_Click_ControlClick() { 116 | this.oAcc[3,2].Click() 117 | DUnit.True(oPaste := this.oAcc.WaitElementExist({Name:"Paste Ctrl+V"},200)) 118 | ;oPaste.ControlClick() 119 | Send "{Esc}" 120 | DUnit.True(oPaste.WaitNotExist()) 121 | } 122 | Test_ObjectFromPoint() { 123 | oEdit := Acc.ObjectFromPoint(100, 200) 124 | DUnit.Equal(oEdit, this.oAcc[4,1,4]) 125 | DUnit.Equal(oEdit.wId, WinExist()) 126 | } 127 | } -------------------------------------------------------------------------------- /Test/Test_Array.ahk: -------------------------------------------------------------------------------- 1 | #include "..\Lib\Array.ahk" 2 | #Include "..\Lib\DUnit.ahk" 3 | 4 | class ArrayTestSuite { 5 | static Fail() { 6 | throw Error() 7 | } 8 | 9 | Test_Slice() { 10 | DUnit.Equal([1].Slice(2), []) 11 | DUnit.Equal([1,2,3,4,5].Slice(2), [2,3,4,5]) 12 | DUnit.Equal([1,2,,4,5].Slice(2), [2,,4,5]) 13 | DUnit.Equal([1,2,,4,5].Slice(1,3), [1,2, unset]) 14 | DUnit.Equal([1,2,3,4,5,6,7].Slice(-1, -1), [7]) 15 | DUnit.Equal([1,2,3,4,5,6,7].Slice(-2, -1), [6,7]) 16 | DUnit.Equal([1,2,3,4,5,6,7].Slice(-10, -1), [1,2,3,4,5,6,7]) 17 | DUnit.Equal([1,2,3,4,5,6,7].Slice(1, -1), [1,2,3,4,5,6,7]) 18 | } 19 | Test_Swap() { 20 | DUnit.Equal([1,2].Swap(1,2), [2,1]) 21 | DUnit.Throws(Array2.Swap.Bind([], 1,3)) 22 | DUnit.Equal([,1].Swap(1,2), [1, unset]) 23 | } 24 | Test_Map() { 25 | DUnit.Equal([].Map(a => a+1), []) 26 | DUnit.Equal([1,2,3].Map(a => a+1), [2,3,4]) 27 | DUnit.Equal([1,,3].Map((a?) => (a ?? 0)+1), [2,1,4]) 28 | DUnit.Equal([1,2,3,4,5].Map((a,b) => a+b, [0,1,3,5,7]), [1,3,6,9,12]) 29 | DUnit.Equal([1,,3,4].Map((a:=0,b:=0) => a+b, [0,1,3,,7]), [1,1,6,4]) 30 | } 31 | Test_ForEach() { 32 | DUnit.Equal([].ForEach(ForEachCallback), []) 33 | DUnit.Equal([1,2,3].ForEach(ForEachCallback), [2,3,4]) 34 | DUnit.Equal([1,,3].ForEach((val?, index?, arr?) => IsSet(val) ? (arr[index] := val+1) : ""), [2,,4]) 35 | ForEachCallback(val, index, arr) { 36 | arr[index] := val+1 37 | } 38 | } 39 | Test_Filter() { 40 | DUnit.Equal([].Filter(IsInteger), []) 41 | DUnit.Equal([1,'two','three',4,5].Filter(IsInteger), [1,4,5]) 42 | DUnit.Equal([1,,2].Filter((v?) => IsSet(v)), [1,2]) 43 | } 44 | Test_Reduce() { 45 | DUnit.Throws(Array2.Reduce.Bind([1,2], 0), "ValueError") 46 | DUnit.Equal([].Reduce((a,b) => (a+b)), '') 47 | DUnit.Equal([1,2,3,4,5].Reduce((a,b) => (a+b)), 15) 48 | DUnit.Equal([1,,3].Reduce((a:=0,b:=0) => (a+b)), 4) 49 | } 50 | Test_IndexOf() { 51 | DUnit.Equal([].IndexOf(2), 0) 52 | DUnit.Equal([1,2,3].IndexOf(2), 2) 53 | DUnit.Equal([1,2,3].IndexOf(2,3), 0) 54 | DUnit.Equal([1,2,3,4,2,1].IndexOf(2,3), 5) 55 | DUnit.Equal([1,,3].IndexOf(unset), 2) 56 | DUnit.Equal([2,1,2].IndexOf(2,-1), 3) 57 | DUnit.Equal([2,1,2].IndexOf(2,-2), 1) 58 | DUnit.Equal([1,1,2].IndexOf(2,-2), 0) 59 | } 60 | Test_Find() { 61 | DUnit.Equal([1,3,5].Find((v) => (Mod(v,2) == 0)), 0) 62 | DUnit.Equal([1,2,3,4,5].Find((v) => (Mod(v,2) == 0)), 2) 63 | _ := [1,2,3,4,5].Find((v) => (Mod(v,2) == 0), &val, 3) 64 | DUnit.Equal(val, 4) 65 | DUnit.Equal([1,2,,4,5].Find((v:=0) => (v == 4)), 4) 66 | DUnit.Equal([1,2,3,4,,7].Find((v:=1) => (Mod(v,2) == 0),, -1), 4) 67 | } 68 | Test_Reverse() { 69 | DUnit.Equal([].Reverse(), []) 70 | DUnit.Equal([1].Reverse(), [1]) 71 | DUnit.Equal([1,2].Reverse(), [2,1]) 72 | DUnit.Equal([1,2,"3","b"].Reverse(), ['b','3',2,1]) 73 | DUnit.Equal([1,2,,4].Reverse(), [4,,2,1]) 74 | } 75 | Test_Count() { 76 | DUnit.Equal([].Count(1), 0) 77 | DUnit.Equal([1,2,2,1,1].Count(1), 3) 78 | DUnit.Equal([1,2,2,1,1].Count((a) => (Mod(a,2) == 0)), 2) 79 | DUnit.Equal([1,,,2].Count(), 2) 80 | } 81 | Test_Sort() { 82 | DUnit.Equal([].Sort(), []) 83 | DUnit.Equal([1].Sort(), [1]) 84 | DUnit.Equal([4,1,3,2].Sort(), [1,2,3,4]) 85 | DUnit.Equal([4,1,3,2].Sort("COn"), [1, 2, 3, 4]) 86 | DUnit.Throws(ObjBindMethod(["a",1,3,2], "Sort")) ; Only numeric values by default 87 | DUnit.Throws(ObjBindMethod(["a",1,3,2], "Sort", "X")) ; Invalid option 88 | DUnit.Equal(["a", 2, 1.2, 1.22, 1.20].Sort("C"), [1.2, 1.2, 1.22, 2, 'a']) 89 | DUnit.Equal(["c", "b", "a", "C", "F", "A"].Sort("C"), ['A', 'C', 'F', 'a', 'b', 'c']) 90 | DUnit.Equal(["0.0", "-0.08", "-0.16", "-0.34"].Sort(), ["-0.34", "-0.16", "-0.08", "0.0"]) 91 | arr := [1,2,3,4,5] 92 | firstProbabilities := [0,0,0,0,0], lastProbabilities := [0,0,0,0,0] 93 | Loop 1000 { 94 | arr.Sort("Random") 95 | firstProbabilities[arr[1]] += 1 96 | lastProbabilities[arr[arr.length]] += 1 97 | } 98 | for v in firstProbabilities 99 | DUnit.Assert(v > 150, "Sort Random might not be random, try running again") 100 | for v in lastProbabilities 101 | DUnit.Assert(v > 150, "Sort Random might not be random, try running again") 102 | 103 | myImmovables:=[] 104 | myImmovables.push({town: "New York", size: "60", price: 400000, balcony: 1}) 105 | myImmovables.push({town: "Berlin", size: "45", price: 230000, balcony: 1}) 106 | myImmovables.push({town: "Moscow", size: "80", price: 350000, balcony: 0}) 107 | myImmovables.push({town: "Tokyo", size: "90", price: 600000, balcony: 2}) 108 | myImmovables.push({town: "Palma de Mallorca", size: "250", price: 1100000, balcony: 3}) 109 | DUnit.Equal(myImmovables.Sort("N R", "size"), [{balcony:3, price:1100000, size:'250', town:'Palma de Mallorca'}, {balcony:2, price:600000, size:'90', town:'Tokyo'}, {balcony:0, price:350000, size:'80', town:'Moscow'}, {balcony:1, price:400000, size:'60', town:'New York'}, {balcony:1, price:230000, size:'45', town:'Berlin'}]) 110 | myImmovables:=[] 111 | myImmovables.push(Map("town", "New York", "size", "60", "price", 400000, "balcony", 1)) 112 | myImmovables.push(Map("town", "Berlin", "size", "45", "price", 230000, "balcony", 1)) 113 | myImmovables.push(Map("town", "Moscow", "size", "80", "price", 350000, "balcony", 0)) 114 | myImmovables.push(Map("town", "Tokyo", "size", "90", "price", 600000, "balcony", 2)) 115 | myImmovables.push(Map("town", "Palma de Mallorca", "size", "250", "price", 1100000, "balcony", 3)) 116 | DUnit.Equal(myImmovables.Sort("N R", "size"), [Map("town", "Palma de Mallorca", "size", "250", "price", 1100000, "balcony", 3), Map("town", "Tokyo", "size", "90", "price", 600000, "balcony", 2), Map("town", "Moscow", "size", "80", "price", 350000, "balcony", 0), Map("town", "New York", "size", "60", "price", 400000, "balcony", 1), Map("town", "Berlin", "size", "45", "price", 230000, "balcony", 1)]) 117 | } 118 | Test_Shuffle() { 119 | DUnit.Equal([].Shuffle(), []) 120 | DUnit.Equal([1].Shuffle(), [1]) 121 | DUnit.Equal([1,2].Shuffle().Length, 2) 122 | } 123 | Test_Join() { 124 | DUnit.Equal([].Join(), "") 125 | DUnit.Equal([1,2,3].Join(), "1,2,3") 126 | DUnit.Equal([1,2,3].Join(""), "123") 127 | DUnit.Equal([1,,3].Join(), "1,,3") 128 | } 129 | Test_Flat() { 130 | DUnit.Equal([].Flat(), []) 131 | DUnit.Equal([1,[2,[3]]].Flat(), [1,2,3]) 132 | DUnit.Equal([1,,2,[3,,4,[5,6]]].Flat(), [1,,2,3,,4,5,6]) 133 | } 134 | Test_Extend() { 135 | DUnit.Equal([].Extend([]), []) 136 | DUnit.Throws(Array2.Extend.Bind([], 0), "ValueError") 137 | DUnit.Equal([,[2]].Extend([3]), [,[2],3]) 138 | } 139 | } -------------------------------------------------------------------------------- /Test/Test_DPI.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | #include "..\Lib\DPI.ahk" 4 | #Include "..\Lib\DUnit.ahk" 5 | #include "..\Lib\FindTextDpi.ahk" 6 | 7 | class DPITestSuite { 8 | static Fail() { 9 | throw Error() 10 | } 11 | 12 | __GetWindow(winTitle, exeName) { 13 | if !WinExist(winTitle) { 14 | Run exeName 15 | WinWaitActive winTitle 16 | Sleep 200 17 | } else 18 | WinActivate winTitle 19 | WinWaitActive winTitle 20 | return WinExist(winTitle) 21 | } 22 | 23 | __GetCalculator() { 24 | hwnd := this.__GetWindow("Calculator", "calc.exe") 25 | WinMove(A_ScreenWidth+100, 100, 300, 500, hwnd) 26 | } 27 | __GetNPP() => this.__GetWindow("ahk_exe notepad++.exe", "notepad++.exe") 28 | 29 | Test_MonitorFuncs() { 30 | DUnit.True(DPI.MonitorFromWindow("A")) 31 | DUnit.Equal(DPI.MonitorFromPoint(100, 100, "client"), DPI.MonitorFromWindow("A")) 32 | CoordMode "Mouse", "Screen" 33 | DUnit.Equal(DPI.GetForMonitor(DPI.MonitorFromPoint(100, 100, "screen")), 144) 34 | DUnit.Equal(DPI.GetForMonitor(DPI.MonitorFromPoint(A_ScreenWidth+100, 100, "screen")), 96) 35 | 36 | monitors := DPI.GetMonitorHandles() 37 | DUnit.Equal(DPI.GetForMonitor(monitors[1]), 144) 38 | DUnit.Equal(DPI.GetForMonitor(monitors[2]), 96) 39 | } 40 | 41 | Test_ClickDPI() { 42 | this.__GetCalculator() 43 | SetMouseDelay 0 44 | SetDefaultMouseSpeed 0 45 | DPI.Click(47, 329) 46 | } 47 | 48 | Test_ImageSearch() { 49 | WinGetPos(&wX, &wY, &wW, &wH, this.__GetCalculator()) 50 | DUnit.True(DPI.ImageSearch(&outX, &outY, 0, 0, wW, wH, "*150 " A_WorkingDir "\..\Resources\DPI_Tutorial\Calculator_icon_225%.png",,216)) 51 | } 52 | 53 | Test_FindText() { 54 | wTitle := "ahk_exe chrome.exe" 55 | WinActivate wTitle 56 | WinWaitActive wTitle 57 | 58 | WinGetPos(&wX, &wY, &wW, &wH, wTitle) 59 | 60 | Text:="|<144>*180$27.00000000000000Dzz01zzs0ADz01bzs0Bzz01zzs0Dzz01XAM0AEX01W4M0ANX01zzs0AEz01W7s0AEz01zzs0Dzz01W7s0AEz01W7s0Dzz01zzs0000000004" 61 | Text:="|<96>*180$13.00003zlDsjwTyDz4ZWGFzsYwGSDz4bWHlzs000000E" 62 | Text:="|<96>*185$21.00000000000000000000000003wE0zq0C7k30S0s7k61y0k006000k0U70C0M3U1ks07y00T0000000000000004" 63 | ;Text:="|<144>**50$29.0000000000000000000000000000001y000Dz300zzC07kDw0C04M0s08k1U0lU703z0C07w0Q0000s0001k0003U0603U0Q0700s0703U07US007zs007zU003w0000000000000000E" 64 | Text:="|<216>**50$41.0000000000000000000000000000000000000y00000DzU2001s3kA00Dzyss00zUDvE03w07wU07U03100S006201g00A403k00k80B0030E0S00A0U0w00zz01M000002U000005000000/000000S000000w000801c000s01s003k03M007U03k00S007k01s007s0Dk007w1z0007TzQ0007U3U0003zw00000T0000000000000000000000000000004" 65 | 66 | ;if (ok:=FindText(&X, &Y, wX, wY, wX+wW, wY+wH, 0.3, 0.2, Text, , 0, , , , , zoomW:=DPI.GetForWindow(wTitle)/216, zoomW)) 67 | ;if (ok:=FindText(&X, &Y, wX, wY, wX+wW, wY+wH, 0.3, 0.3, "##10$Calculator_icon_225%.png", , 0, , , , , zoomW:=DPI.GetForWindow(wTitle)/216, zoomW)) 68 | ok:=FindText(&X, &Y, wX, wY, wX+wW, wY+wH, 0.2, 0.1, "##10$" A_WorkingDir "\..\Resources\DPI_Tutorial\Chrome_Reload_100%.png", , 0, , , , , zoomW:=DPI.GetForWindow(wTitle)/96, zoomW) 69 | DUnit.True(ok) 70 | } 71 | 72 | Test_CoordConversion() { 73 | this.__GetCalculator() 74 | 75 | SetMouseDelay 0 76 | SetDefaultMouseSpeed 0 77 | DPI.MouseMove(47, 329) 78 | 79 | CoordMode "Mouse", "Client" 80 | MouseGetPos(&clientX, &clientY) 81 | CoordMode "Mouse", "Window" 82 | MouseGetPos(&windowX, &windowY) 83 | CoordMode "Mouse", "Screen" 84 | MouseGetPos(&screenX, &screenY) 85 | 86 | CoordMode "Mouse", "Client" 87 | MouseGetPos(&newX, &newY) 88 | DPI.CoordsToClient(&newX, &newY, A_CoordModeMouse) 89 | DUnit.Equal(newX, clientX), DUnit.Equal(newY, clientY) 90 | 91 | CoordMode "Mouse", "Window" 92 | MouseGetPos(&newX, &newY) 93 | DPI.CoordsToWindow(&newX, &newY, A_CoordModeMouse) 94 | DUnit.Equal(newX, windowX), DUnit.Equal(newY, windowY) 95 | 96 | CoordMode "Mouse", "Screen" 97 | MouseGetPos(&newX, &newY) 98 | DPI.CoordsToScreen(&newX, &newY, A_CoordModeMouse) 99 | DUnit.Equal(newX, screenX), DUnit.Equal(newY, screenY) 100 | } 101 | 102 | Test_GetForWindow() { 103 | DUnit.Equal(DPI.GetForWindow(this.__GetNPP()), A_ScreenDPI) 104 | DUnit.Equal(DPI.GetForWindow(hwnd := this.__GetCalculator()), DPI.GetForMonitor(DPI.MonitorFromWindow(hwnd))) 105 | } 106 | 107 | Test_GuiOptScale() { 108 | DUnit.Equal(DPI.GuiOptScale("w100 h-1", 144), "w150 h-1") 109 | DPI.Standard := 144 110 | DUnit.Equal(DPI.GuiOptScale("w100 h-1", 144), "w100 h-1") 111 | DPI.Standard := 96 112 | } 113 | } -------------------------------------------------------------------------------- /Test/Test_Map.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | #include "..\Lib\Map.ahk" 4 | #Include "..\Lib\DUnit.ahk" 5 | 6 | class MapTestSuite { 7 | static Fail() { 8 | throw Error() 9 | } 10 | 11 | Begin() { 12 | this.EmptyMap := Map() 13 | this.Map := Map("a", 1, "b", 2, "c", 3) 14 | } 15 | 16 | Test_Keys() { 17 | DUnit.Equal(this.EmptyMap.Keys, []) 18 | DUnit.Equal(this.Map.Keys, ["a", "b", "c"]) 19 | } 20 | Test_Values() { 21 | DUnit.Equal(this.EmptyMap.Values, []) 22 | DUnit.Equal(this.Map.Values, [1,2,3]) 23 | } 24 | Test_Map() { 25 | DUnit.Equal(this.EmptyMap.Map((k,a) => a+1), Map()) 26 | DUnit.Equal(this.Map.Map((k,a) => a+1), Map("a", 2, "b", 3, "c", 4)) 27 | DUnit.Equal(Map("a", 1, "b", 2, "c", 3).Map((k,v1,v2) => v1+v2, Map("a", 2, "b", 2, "c", 2)), Map("a", 3, "b", 4, "c", 5)) 28 | } 29 | Test_ForEach() { 30 | DUnit.Equal(this.EmptyMap.ForEach(ForEachCallback), Map()) 31 | DUnit.Equal(this.Map.ForEach(ForEachCallback), Map("a", 2, "b", 3, "c", 4)) 32 | ForEachCallback(val, key, m) { 33 | m[key] := val+1 34 | } 35 | } 36 | Test_Filter() { 37 | DUnit.Equal(this.EmptyMap.Filter(IsInteger), Map()) 38 | DUnit.Equal(Map("a", 1, "b", "b", "c", 3, "d", "d").Filter((k,v) => IsInteger(v)), Map("a", 1, "c", 3)) 39 | } 40 | Test_Find() { 41 | DUnit.Equal(Map("a", 1, "b", 2, "c", 3).Find((v) => (Mod(v,2) == 0)), "b") 42 | } 43 | Test_Count() { 44 | DUnit.Equal(Map().Count(1), 0) 45 | DUnit.Equal(Map("a", 1, "b", 1, "c", 2).Count(1), 2) 46 | DUnit.Equal(Map("a", 1, "b", 1, "c", 2).Count((a) => (Mod(a,2) == 0)), 1) 47 | } 48 | Test_Merge() { 49 | DUnit.Equal(Map("a", 1, "b", 2).Merge(Map("c", 3)), this.Map) 50 | DUnit.Throws(Map2.Merge.Bind(Map(), 0), "ValueError") 51 | DUnit.Equal(Map("a", 1, "b", 2).Merge([3]), Map("a", 1, "b", 2, 1, 3)) 52 | } 53 | } -------------------------------------------------------------------------------- /Test/Test_Misc.ahk: -------------------------------------------------------------------------------- 1 | #include "..\Lib\Misc.ahk" 2 | ;#Include "..\Lib\DUnit.ahk" 3 | ;DUnit("C", MiscTestSuite) 4 | 5 | class MiscTestSuite { 6 | static Fail() { 7 | throw Error() 8 | } 9 | Begin() { 10 | DUnit.Print(,"", "") 11 | } 12 | 13 | Test_Print() { 14 | DUnit.Equal(DUnit.Print([]), "[]") 15 | DUnit.Equal(DUnit.Print(Map()), "Map()") 16 | DUnit.Equal(DUnit.Print({}), "{}") 17 | DUnit.Equal(DUnit.Print([1]), "[1]") 18 | DUnit.Equal(DUnit.Print(Map("key", "value")), "Map('key':'value')") 19 | DUnit.Equal(DUnit.Print({key:"value"}), "{key:'value'}") 20 | DUnit.Equal(DUnit.Print([1,[2,[3,4]]]), "[1, [2, [3, 4]]]") 21 | DUnit.Equal(DUnit.Print(Map(1, 2, "3", "4")), "Map(1:2, '3':'4')") 22 | DUnit.Equal(DUnit.Print({key:"value", 1:2, 3:"4"}), "{1:2, 3:'4', key:'value'}") 23 | } 24 | 25 | Test_Swap() { 26 | a := 1, b := 2 27 | Swap(&a, &b) 28 | DUnit.Equal(a, 2) 29 | DUnit.Equal(b, 1) 30 | } 31 | 32 | Test_Range() { 33 | DUnit.Equal(DUnit.Print(Range(5)), "Range(1:1, 2:2, 3:3, 4:4, 5:5)") 34 | DUnit.Equal(Range(5).ToArray(), [1,2,3,4,5]) 35 | DUnit.Equal(DUnit.Print(Range(0)), "Range(1:1, 2:0)") 36 | } 37 | Test_Range2() { ; Split into two because of the ListLines limitation 38 | DUnit.Equal(DUnit.Print(Range(3, 5)), "Range(1:3, 2:4, 3:5)") 39 | DUnit.Equal(DUnit.Print(Range(5, 3)), "Range(1:5, 2:4, 3:3)") 40 | DUnit.Equal(DUnit.Print(Range(5,,2)), "Range(1:1, 2:3, 3:5)") 41 | DUnit.Equal(DUnit.Print(Range(5,-5,-2)), "Range(1:5, 2:3, 3:1, 4:-1, 5:-3, 6:-5)") 42 | } 43 | 44 | Test_RegExMatchAll() { 45 | DUnit.Equal(RegExMatchAll("", "\w+"), []) 46 | DUnit.Equal(DUnit.Print(RegExMatchAll("a,bb,ccc", "\w+")), "[RegExMatchInfo(0:'a'), RegExMatchInfo(0:'bb'), RegExMatchInfo(0:'ccc')]") 47 | DUnit.Equal(DUnit.Print(RegExMatchAll("a,bb,ccc", "\w+",4)), "[RegExMatchInfo(0:'b'), RegExMatchInfo(0:'ccc')]") 48 | } 49 | 50 | Test_ConvertWinPos() { 51 | ccm := A_CoordModeMouse 52 | WinExist("A") 53 | A_CoordModeMouse := "client" 54 | MouseMove(100, 200) 55 | MouseGetPos(&clientX, &clientY) 56 | A_CoordModeMouse := "screen" 57 | MouseGetPos(&screenX, &screenY) 58 | A_CoordModeMouse := "window" 59 | MouseGetPos(&windowX, &windowY) 60 | A_CoordModeMouse := ccm 61 | ConvertWinPos(screenX, screenY, &OutX, &OutY, "screen", "client", "A") 62 | DUnit.Equal(clientX " " clientY, OutX " " OutY) 63 | ConvertWinPos(clientX, clientY, &OutX, &OutY, "client", "screen", "A") 64 | DUnit.Equal(screenX " " screenY, OutX " " OutY) 65 | ConvertWinPos(windowX, windowY, &OutX, &OutY, "window", "screen", "A") 66 | DUnit.Equal(screenX " " screenY, OutX " " OutY) 67 | ConvertWinPos(screenX, screenY, &OutX, &OutY, "screen", "window", "A") 68 | DUnit.Equal(windowX " " windowY, OutX " " OutY) 69 | ConvertWinPos(clientX, clientY, &OutX, &OutY, "client", "window", "A") 70 | DUnit.Equal(windowX " " windowY, OutX " " OutY) 71 | ConvertWinPos(windowX, windowY, &OutX, &OutY, "window", "client", "A") 72 | DUnit.Equal(clientX " " clientY, OutX " " OutY) 73 | } 74 | } -------------------------------------------------------------------------------- /Test/Test_String.ahk: -------------------------------------------------------------------------------- 1 | #include "..\Lib\String.ahk" 2 | 3 | class StringTestSuite { 4 | static Fail() { 5 | throw Error() 6 | } 7 | 8 | Test_Substring() { 9 | DUnit.Equal(""[1], "") 10 | DUnit.Equal("abcd"[2,3], "bc") 11 | DUnit.Equal("abcd"[-3,-1], "bcd") 12 | } 13 | Test___Item() { 14 | DUnit.Equal("abcd"[2,3], "bc") 15 | DUnit.Equal("12ab45"[1,3], "12a") 16 | DUnit.Equal("12ab45"[-1,-3], "54b") 17 | DUnit.Equal("12ab45"[1,1], "1") 18 | } 19 | Test___Enum() { 20 | res := "" 21 | for c in "abcd" 22 | res .= c " " 23 | DUnit.Equal(res, "a b c d ") 24 | res := "" 25 | for i, c in "abcd" 26 | res .= i ":" c " " 27 | DUnit.Equal(res, "1:a 2:b 3:c 4:d ") 28 | res := "" 29 | for c in "" 30 | res .= c " " 31 | DUnit.Equal(res, "") 32 | } 33 | Test_Length() { 34 | DUnit.Equal("".Length, 0) 35 | DUnit.Equal("abc".Length, 3) 36 | DUnit.Equal("💩abc".Length, 5) 37 | DUnit.Equal("💩abc".WLength, 4) 38 | DUnit.Equal("💩abc".ULength, 4) 39 | DUnit.Equal("👨‍👩‍👧‍👦".ULength, 1) 40 | DUnit.Equal("Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞".ULength, 6) 41 | ;DUnit.Equal("👨‍👩‍👧‍👦".WLength, 1) 42 | } 43 | 44 | Test_ToUpper() { 45 | DUnit.Equal("abc".ToUpper(), "ABC") 46 | DUnit.Equal("Abc1 ".ToUpper(), "ABC1 ") 47 | DUnit.Equal("".ToUpper(), "") 48 | } 49 | Test_ToLower() { 50 | DUnit.Equal("ABC".ToLower(), "abc") 51 | DUnit.Equal("AbC1 ".ToLower(), "abc1 ") 52 | DUnit.Equal("".ToLower(), "") 53 | } 54 | Test_ToTitle() { 55 | DUnit.Equal("abc".ToTitle(), "Abc") 56 | DUnit.Equal("f-sharp".ToTitle(), "F-sharp") 57 | DUnit.Equal("This is a 11-test.".ToTitle(), "This Is A 11-Test.") 58 | DUnit.Equal("".ToTitle(), "") 59 | } 60 | Test_Split() { 61 | DUnit.Equal("abc,def,ghi".Split(","), ["abc","def","ghi"]) 62 | DUnit.Equal("💩💩emoji".Split("💩em"), ["💩", "oji"]) 63 | ;OutputDebug(SubStr("💩", 1,1)) 64 | ;DUnit.Equal("abc💩def💩ghi".Split("💩"), ["abc","def","ghi"]) 65 | DUnit.Equal("abc,def,N ghi n".Split(",", "N n"), ["abc","def","ghi"]) 66 | DUnit.Equal("abc,N def, nghi n".Split(","," nN",2), ["abc","def, nghi"]) 67 | } 68 | Test_Trim() { 69 | DUnit.Equal("".Trim(), "") 70 | DUnit.Equal(" abcd ".Trim(" ad"), "bc") 71 | } 72 | Test_LTrim() { 73 | DUnit.Equal("".LTrim(), "") 74 | DUnit.Equal(" abcd ".LTrim(" ad"), "bcd ") 75 | } 76 | Test_RTrim() { 77 | DUnit.Equal("".RTrim(), "") 78 | DUnit.Equal(" abcd ".RTrim(" ad"), " abc") 79 | } 80 | Test_Compare() { 81 | DUnit.Equal("".Compare(""), 0) 82 | DUnit.Equal("Abc".Compare("abc", 1), -1) 83 | DUnit.Equal("A10".Compare("A2", "Logical"), 1) 84 | } 85 | Test_Format() { 86 | DUnit.Equal("{2}, {1}!".Format("World", "Hello"), "Hello, World!") 87 | DUnit.Equal("{:-10}".Format("Left"), "Left ") 88 | DUnit.Equal("{:10}".Format("Right"), " Right") 89 | DUnit.Equal("{1:#x} {2:X} 0x{3:x}".Format(3735928559, 195948557, 0), "0xdeadbeef BADF00D 0x0") 90 | DUnit.Equal("{1:0.3f} {1:.10f}".Format(3.141592654), "3.142 3.1415926540") 91 | } 92 | Test_Sort() { 93 | DUnit.Equal("".Sort(), "") 94 | DUnit.Equal("5,3,7,9,1,13,999,-4".Sort("N D,"), "-4,1,3,5,7,9,13,999") 95 | DUnit.Equal("5,3,7,9,1,1,13,999,-4".Sort("U D,", (a,b,*) => (a < b ? 1 : a > b ? -1 : 0)), "999,13,9,7,5,3,1,-4") 96 | DUnit.Equal("A3`nA1`nZ70`nZ070".Sort("CLogical"), "A1`nA3`nZ070`nZ70") 97 | } 98 | Test_InStr() { 99 | DUnit.Equal("".Find("b"), 0) 100 | DUnit.Equal("abc".Find("b"), 2) 101 | } 102 | Test_RegExMatch() { 103 | DUnit.Equal("abc".RegexMatch("b")[], "b") 104 | DUnit.Equal("abc".RegexMatchAll("b")[1][], "b") 105 | DUnit.Equal("abc".RegexMatchAll("b").Length, 1) 106 | DUnit.Equal("abc".RegexMatchAll("b|c").Length, 2) 107 | } 108 | Test_RegExReplace() { 109 | DUnit.Equal("abc".RegexReplace("b"), "ac") 110 | DUnit.Equal("abc".RegexReplace(["b"]), "ac") 111 | DUnit.Equal("abc".RegexReplace(["a", "(c)"], ["", "$1$1"]), "bcc") 112 | } 113 | Test_SplitPath() { 114 | DUnit.Equal("C:\My Documents\Address List.txt".SplitPath(), {FileName: "Address List.txt", Dir: "C:\My Documents", Ext: "txt", NameNoExt: "Address List", Drive: "C:"}) 115 | } 116 | Test_Replace() { 117 | ;Replace(Needle [, ReplaceText, CaseSense, &OutputVarCount, Limit]) 118 | DUnit.Equal("ABCabc".Replace("abc", "cba", 1), "ABCcba") 119 | DUnit.Equal("ABCabc".Replace("abc", "cba", "Off", &count), "cbacba") 120 | DUnit.Equal(count, 2) 121 | DUnit.Equal("Num1num2num3num2".Replace("num2", "💩",,,1), "Num1💩num3num2") 122 | } 123 | Test_LPad() { 124 | DUnit.Equal("aaa".LPad("+", -1), "aaa") 125 | DUnit.Equal("aaa".LPad("+", 5), "+++++aaa") 126 | } 127 | Test_RPad() { 128 | DUnit.Equal("aaa".RPad("+", -1), "aaa") 129 | DUnit.Equal("aaa".RPad("+", 5), "aaa+++++") 130 | } 131 | Test_Count() { 132 | DUnit.Equal("12234".Count("5"), 0) 133 | DUnit.Equal("12234".Count("2"), 2) 134 | } 135 | Test_Repeat() { 136 | DUnit.Equal("abc".Repeat(0), "") 137 | DUnit.Equal("a}".Repeat(1), "a}") 138 | DUnit.Equal("abc".Repeat(3), "abcabcabc") 139 | } 140 | Test_Reverse() { 141 | DUnit.Equal("".Reverse(), "") 142 | DUnit.Equal("Olé".Reverse(), "élO") 143 | } 144 | Test_WReverse() { 145 | DUnit.Equal("".WReverse(), "") 146 | DUnit.Equal("ab`nc1💩".WReverse(), "💩1c`nba") 147 | } 148 | Test_Insert() { 149 | DUnit.Equal("abcdef".Insert("123"), "123abcdef") 150 | DUnit.Equal("abc".Insert("d", 0), "abcd") 151 | DUnit.Equal("abc".Insert("d", -1), "abdc") 152 | } 153 | Test_Overwrite() { 154 | DUnit.Equal("aaabbbccc".Overwrite("zzz", 4), "aaazzzccc") 155 | DUnit.Equal("123bbbccc".Overwrite("zzz", 10), "") 156 | DUnit.Equal("aaabbbccc".Overwrite("zzz", 0), "aaabbbccczzz") 157 | DUnit.Equal("aaabbbccc".Overwrite("zzz", -3), "aaabbbzzz") 158 | } 159 | Test_Delete() { 160 | DUnit.Equal("aaabbbccc".Delete(4, 3), "aaaccc") 161 | DUnit.Equal("123456".Delete(-3, 3), "123") 162 | DUnit.Equal("aaabbbccc".Delete(-1, 2), "aaabbbcc") 163 | } 164 | Test_LineWrap() { 165 | DUnit.Equal("Apples are a round fruit, usually red".LineWrap(20, "---"), "Apples are a round f`n---ruit, usually red") 166 | } 167 | Test_WordWrap() { 168 | DUnit.Throws(String2.WordWrap.Bind("abc", "a"), "TypeError") 169 | DUnit.Equal("Apples are a round fruit, usually red.".WordWrap(20, "---"), "Apples are a round`n---fruit, usually`n---red.") 170 | } 171 | Test_InsertLine() { 172 | DUnit.Equal("aaa|ccc|ddd".InsertLine("bbb", 2, "|"), "aaa|bbb|ccc|ddd") 173 | DUnit.Equal("aaa`n`rbbb`n`rccc".InsertLine("ddd", 0), "aaa`nbbb`nccc`nddd") 174 | DUnit.Equal("aaa|bbb|ccc".InsertLine("ddd", -1, "|"), "aaa|bbb|ddd|ccc") 175 | DUnit.Equal("aaa|ccc|ddd".InsertLine("bbb", 5, "|"), "aaa|ccc|ddd||bbb") 176 | } 177 | Test_DeleteLine() { 178 | DUnit.Throws(String2.DeleteLine.Bind("abc", 0, "a"), "ValueError") 179 | DUnit.Equal("aaa|bbb|777|ccc".DeleteLine(3, "|"), "aaa|bbb|ccc") 180 | DUnit.Equal("aaa|bbb|777|ccc".DeleteLine(-1, "|"), "aaa|bbb|777") 181 | } 182 | Test_ReadLine() { 183 | DUnit.Throws(String2.ReadLine.Bind("abc", 0, "a"), "ValueError") 184 | DUnit.Equal("aaa|bbb|ccc|ddd|eee|fff".ReadLine(4, "|"), "ddd") 185 | DUnit.Equal("aaa|bbb|ccc|ddd|eee|fff".ReadLine(-1, "|"), "fff") 186 | } 187 | Test_RemoveDuplicates() { 188 | DUnit.Equal("aaa|bbb|||ccc||ddd".RemoveDuplicates("|"), "aaa|bbb|ccc|ddd") 189 | DUnit.Equal("abc\\\cde".RemoveDuplicates("\"), "abc\cde") 190 | DUnit.Equal("abc`n`ncde".RemoveDuplicates("`n"), "abc`ncde") 191 | DUnit.Equal("abc(\)}(\)}cde".RemoveDuplicates("(\)}"), "abc(\)}cde") 192 | } 193 | Test_Contains() { 194 | DUnit.True("aaa|bbb|ccc|ddd".Contains("eee", "aaa")) 195 | DUnit.False("aaa|bbb|ccc|ddd".Contains("eee")) 196 | } 197 | Test_Center() { 198 | DUnit.Equal("".Center(), "") 199 | DUnit.Equal("aaa`na`naaaaaaaa".Center(), " aaa`n a`naaaaaaaa") 200 | DUnit.Equal("aaa`na`naaaaaaaa".Center(,1), " aaa `n a `naaaaaaaa") 201 | DUnit.Equal("aaa`na`naaaaaaaa".Center(,1,,,9), " aaa `n a `naaaaaaaa ") 202 | } 203 | Test_Right() { 204 | DUnit.Equal("".Right(), "") 205 | DUnit.Equal("aaa`na`naaaaaaaa".Right(), " aaa`n a`naaaaaaaa") 206 | DUnit.Equal("aaa`na`naaaaaaa".Right(), " aaa`n a`naaaaaaa") 207 | } 208 | Test_Is() { 209 | DUnit.True("abc".IsAlnum) 210 | DUnit.True("abc1".IsAlnum) 211 | DUnit.False("abc.".IsAlnum) 212 | DUnit.True("0".IsDigit) 213 | } 214 | Test_Concat() { 215 | DUnit.Equal("|".Concat("111", "222", "333"), "111|222|333") 216 | DUnit.Equal("d".Concat("ddd", "ddd"), "ddddddd") 217 | DUnit.Equal("
".Concat("break", "br"), "break
br") 218 | } 219 | } -------------------------------------------------------------------------------- /Tools/WindowSpyDpi.ahk: -------------------------------------------------------------------------------- 1 | ; 2 | ; Window Spy for AHKv2 3 | ; 4 | 5 | #Requires AutoHotkey v2.0 6 | 7 | #NoTrayIcon 8 | #SingleInstance Ignore 9 | SetWorkingDir A_ScriptDir 10 | CoordMode "Pixel", "Screen" 11 | 12 | Global oGui, A_StandardDpi := 96 13 | 14 | WinSpyGui() 15 | 16 | WinSpyGui() { 17 | Global oGui 18 | 19 | try TraySetIcon "inc\spy.ico" 20 | DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy") 21 | 22 | oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale","Window Spy for AHKv2") 23 | oGui.OnEvent("Close",WinSpyClose) 24 | oGui.OnEvent("Size",WinSpySize) 25 | 26 | oGui.Add("Text",,"Window Title, Class and Process:") 27 | oGui.Add("Checkbox","yp xp+200 w120 Right vCtrl_FollowMouse","Follow Mouse").Value := 1 28 | oGui.Add("Edit","xm w320 r5 ReadOnly -Wrap vCtrl_Title") 29 | oGui.Add("Text",,"Mouse Position") 30 | oGui.Add("Checkbox","yp xp+115 w80 Right vCtrl_DPIAware","DPI Aware").Value := 1 31 | ToggleDPIAwarenessContext(oGui["Ctrl_DPIAware"]) 32 | oGui["Ctrl_DPIAware"].OnEvent("Click", ToggleDPIAwarenessContext) 33 | oGui.Add("Checkbox","yp xp+85 w125 Right vCtrl_DPINormalized","DPI Normalized To " A_StandardDpi).Value := 1 34 | oGui.Add("Edit","xm w320 r4 ReadOnly vCtrl_MousePos") 35 | oGui.Add("Text","w200 vCtrl_CtrlLabel",(txtFocusCtrl := "Focused Control") ":") 36 | oGui.Add("Edit","w320 r4 ReadOnly vCtrl_Ctrl") 37 | oGui.Add("Text",,"Active Window Postition:") 38 | oGui.Add("Edit","w320 r2 ReadOnly vCtrl_Pos") 39 | oGui.Add("Text",,"Status Bar Text:") 40 | oGui.Add("Edit","w320 r2 ReadOnly vCtrl_SBText") 41 | oGui.Add("Checkbox","vCtrl_IsSlow","Slow TitleMatchMode") 42 | oGui.Add("Text",,"Visible Text:") 43 | oGui.Add("Edit","w320 r2 ReadOnly vCtrl_VisText") 44 | oGui.Add("Text",,"All Text:") 45 | oGui.Add("Edit","w320 r2 ReadOnly vCtrl_AllText") 46 | oGui.Add("Text","w320 r1 vCtrl_Freeze",(txtNotFrozen := "(Hold Ctrl or Shift to suspend updates)")) 47 | 48 | oGui.Show("NoActivate") 49 | WinGetClientPos(&x_temp, &y_temp2,,,"ahk_id " oGui.hwnd) 50 | 51 | ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX 52 | 53 | oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use 54 | oGui.txtFrozen := "(Updates suspended)" 55 | oGui.txtMouseCtrl := "Control Under Mouse Position" 56 | oGui.txtFocusCtrl := txtFocusCtrl 57 | 58 | SetTimer Update, 250 59 | } 60 | 61 | WinSpySize(GuiObj, MinMax, Width, Height) { 62 | Global oGui 63 | 64 | If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is 65 | return 66 | 67 | SetTimer Update, (MinMax=0)?250:0 ; suspend updates on minimize 68 | 69 | ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin 70 | list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze" 71 | Loop Parse list, "," 72 | oGui["Ctrl_" A_LoopField].Move(,,ctrlW) 73 | } 74 | 75 | ToggleDPIAwarenessContext(CtrlObj, *) { 76 | static MaximumDPIAwarenessContext := VerCompare(A_OSVersion, ">=10.0.15063") ? -4 : -3, RestoreDPIAwareness 77 | RestoreDPIAwareness := DllCall("SetThreadDpiAwarenessContext", "ptr", CtrlObj.Value ? MaximumDPIAwarenessContext : RestoreDPIAwareness) 78 | } 79 | 80 | WinSpyClose(GuiObj) { 81 | ExitApp 82 | } 83 | 84 | Update() { ; timer, no params 85 | Try TryUpdate() ; Try 86 | } 87 | 88 | TryUpdate() { 89 | Global oGui 90 | 91 | If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is 92 | return 93 | 94 | Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value 95 | CoordMode "Mouse", "Screen" 96 | MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow 97 | actWin := WinExist("A") 98 | 99 | if (Ctrl_FollowMouse) { 100 | curWin := msWin, curCtrl := msCtrl 101 | WinExist("ahk_id " curWin) ; updating LastWindowFound? 102 | } else { 103 | curWin := actWin 104 | curCtrl := ControlGetFocus() ; get focused control hwnd from active win 105 | } 106 | curCtrlClassNN := "" 107 | Try curCtrlClassNN := ControlGetClassNN(curCtrl) 108 | 109 | t1 := WinGetTitle(), t2 := WinGetClass() 110 | if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab 111 | UpdateText("Ctrl_Freeze", oGui.txtFrozen) 112 | return 113 | } 114 | 115 | UpdateText("Ctrl_Freeze", oGui.txtNotFrozen) 116 | t3 := WinGetProcessName(), t4 := WinGetPID() 117 | 118 | WinDataText := t1 "`n" ; ZZZ 119 | . "ahk_class " t2 "`n" 120 | . "ahk_exe " t3 "`n" 121 | . "ahk_pid " t4 "`n" 122 | . "ahk_id " curWin 123 | 124 | UpdateText("Ctrl_Title", WinDataText) 125 | CoordMode "Mouse", "Window" 126 | MouseGetPos &mrX, &mrY 127 | CoordMode "Mouse", "Client" 128 | MouseGetPos &mcX, &mcY 129 | mClr := PixelGetColor(msX,msY,"RGB") 130 | 131 | if oGui["Ctrl_DPINormalized"].Value { 132 | wDpi := WinGetDpi("ahk_id " curWin) 133 | DpiToStandard(wDpi, &mrX, &mrY), DpiToStandard(wDpi, &mcX, &mcY) 134 | } 135 | 136 | mpText := "Screen:`t" msX ", " msY "`n" 137 | . "Window:`t" mrX ", " mrY "`n" 138 | . "Client:`t" mcX ", " mcY " (default)`n" 139 | . "Color:`t" mClr " (Red=" SubStr(mClr, 3, 2) " Green=" SubStr(mClr, 5, 2) " Blue=" SubStr(mClr, 7) ")" 140 | 141 | UpdateText("Ctrl_MousePos", mpText) 142 | 143 | UpdateText("Ctrl_CtrlLabel", (Ctrl_FollowMouse ? oGui.txtMouseCtrl : oGui.txtFocusCtrl) ":") 144 | 145 | if (curCtrl) { 146 | ctrlTxt := ControlGetText(curCtrl) 147 | WinGetClientPos(&sX, &sY, &sW, &sH, curCtrl) 148 | ControlGetPos &cX, &cY, &cW, &cH, curCtrl 149 | if oGui["Ctrl_DPINormalized"].Value 150 | DpiToStandard(wDpi, &cX, &cY), DpiToStandard(wDpi, &cW, &cH) 151 | 152 | cText := "ClassNN:`t" curCtrlClassNN "`n" 153 | . "Text:`t" textMangle(ctrlTxt) "`n" 154 | . "Screen:`tx: " sX "`ty: " sY "`tw: " sW "`th: " sH "`n" 155 | . "Client`tx: " cX "`ty: " cY "`tw: " cW "`th: " cH 156 | } else 157 | cText := "" 158 | 159 | UpdateText("Ctrl_Ctrl", cText) 160 | wX := "", wY := "", wW := "", wH := "" 161 | WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin 162 | WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin) 163 | 164 | if oGui["Ctrl_DPINormalized"].Value 165 | DpiToStandard(wDpi, &wW, &wH), DpiToStandard(wDpi, &wcW, &wcH) 166 | 167 | wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n" 168 | . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH 169 | 170 | UpdateText("Ctrl_Pos", wText) 171 | sbTxt := "" 172 | 173 | Loop { 174 | ovi := "" 175 | Try ovi := StatusBarGetText(A_Index) 176 | if (ovi = "") 177 | break 178 | sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n" 179 | } 180 | 181 | sbTxt := SubStr(sbTxt,1,-1) ; StringTrimRight, sbTxt, sbTxt, 1 182 | UpdateText("Ctrl_SBText", sbTxt) 183 | bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow 184 | 185 | if (bSlow) { 186 | DetectHiddenText False 187 | ovVisText := WinGetText() ; WinGetText, ovVisText 188 | DetectHiddenText True 189 | ovAllText := WinGetText() ; WinGetText, ovAllText 190 | } else { 191 | ovVisText := WinGetTextFast(false) 192 | ovAllText := WinGetTextFast(true) 193 | } 194 | 195 | UpdateText("Ctrl_VisText", ovVisText) 196 | UpdateText("Ctrl_AllText", ovAllText) 197 | } 198 | 199 | ; =========================================================================================== 200 | ; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects 201 | ; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used 202 | ; to retrieve the text of each control. 203 | ; =========================================================================================== 204 | WinGetTextFast(detect_hidden) { 205 | controls := WinGetControlsHwnd() 206 | 207 | static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source. 208 | 209 | buf := Buffer(WINDOW_TEXT_SIZE * 2,0) 210 | 211 | text := "" 212 | 213 | Loop controls.Length { 214 | hCtl := controls[A_Index] 215 | if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl) 216 | continue 217 | if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE) 218 | continue 219 | 220 | text .= StrGet(buf) "`r`n" ; text .= buf "`r`n" 221 | } 222 | return text 223 | } 224 | 225 | ; =========================================================================================== 226 | ; Unlike using a pure GuiControl, this function causes the text of the 227 | ; controls to be updated only when the text has changed, preventing periodic 228 | ; flickering (especially on older systems). 229 | ; =========================================================================================== 230 | UpdateText(vCtl, NewText) { 231 | Global oGui 232 | static OldText := {} 233 | ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd) 234 | 235 | if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) { 236 | ctl.Value := NewText 237 | OldText.%hCtl% := NewText 238 | } 239 | } 240 | 241 | WinGetDpi(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?) { 242 | hMonitor := DllCall("MonitorFromWindow", "ptr", WinExist(WinTitle?, WinText?, ExcludeTitle?, ExcludeText?), "int", 2, "ptr") ; MONITOR_DEFAULTTONEAREST 243 | DllCall("Shcore.dll\GetDpiForMonitor", "ptr", hMonitor, "int", 0, "uint*", &dpiX:=0, "uint*", &dpiY:=0) 244 | return dpiX 245 | } 246 | DpiToStandard(dpi, &x, &y) => (x := DllCall("MulDiv", "int", x, "int", A_StandardDpi, "int", dpi, "int"), y := DllCall("MulDiv", "int", y, "int", A_StandardDpi, "int", dpi, "int")) 247 | 248 | textMangle(x) { 249 | elli := false 250 | if (pos := InStr(x, "`n")) 251 | x := SubStr(x, 1, pos-1), elli := true 252 | else if (StrLen(x) > 40) 253 | x := SubStr(x,1,40), elli := true 254 | if elli 255 | x .= " (...)" 256 | return x 257 | } 258 | 259 | suspend_timer() { 260 | Global oGui 261 | SetTimer Update, 0 262 | UpdateText("Ctrl_Freeze", oGui.txtFrozen) 263 | } 264 | 265 | ~*Shift:: 266 | ~*Ctrl::suspend_timer() 267 | 268 | ~*Ctrl up:: 269 | ~*Shift up::SetTimer Update, 250 --------------------------------------------------------------------------------