Configuration Options:
- Trigger Length
One Press Auto-completion (Toggle) *default:TRUE Enter autocompletes+newline, Tab autocompletes+space
Process Exclusion
Window Title Exclusion
All/Compatible-Only Window Auto-completion (Toggle)
Case Correction (Toggle)
Case Ranking of Suggestions (Toggle)
Remove Punctuation from matches (Toggle) *default: FALSE
Interaction is via Up/Down/Enter/Tab, the later two behaving differently based on configuration,see above.
Merci A_AhkUser for insight,suggestive corrections and great library which motivated me to rewrite my tooltip version, now at the bottom of post.
Let me know about any bugs.
Legacy toolTip version in my OP can be found at the bottom.
EDIT: Saturday, May 26, 2018 16:42
- Full Rewrite.
Added:
- Support for unknown keyboard layouts. Working On It(Experimental).
A number of configuration options, see above.
More endkeys; Left,Right,LButton,RButton
A lot of little things under the hood....
- Use ByRef where necessary for performance. Thx @A_AhkUser for pointing it out.
Fixed: Issues with #If not properly working, it was a conflict with another script on my end.
EDIT: Thursday, May 31, 2018
Fixed: ListBox error message.
Fixed: ListBox flickering on window change.
EDIT: Saturday, June 02, 2018
Just added version information, and optimised some of the logic.
Edit: Monday, June 04, 2018
v1.1
Added: sanityCheck to ensure script behaviour is as predictable as possible.
Fixed: ListBox being generated beyond the edges of the screen when typing at the edges of the display.
Fixed: Endkey backspace hotkey thread no longer interrupted, ensures backspace works as expected.
Fixed: Listbox hotkey threads no longer interrupted, ensures Tab/Enter work as expected.
Added: A few more endkeys to cover more use cases,such as Numpad Arrows.
Code: Select all
; v1.1
;
; brutus_skywalker
;
; AutoHotkey: v1.1.26.01 Unicode 32-bit (Installed)
; Language: English
; OS: WIN_7 64-bit Service Pack 1 v6.1.7601 (WIN32_NT) , 6050.68MB RAM
; Date: Sunday, May 27, 2018 12:34
;
;https://autohotkey.com/boards/viewtopic.php?f=6&t=48915
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
#SingleInstance, Force
; #NoTrayIcon
;==================================== CONFIGURATION START =================================================
;Number of input strings to trigger AutoCompletion Suggestions
triggerLength := 2
;processes to exclude
excludedProcessesList = notepad++.exe,sublime_text.exe ;these examples have their own autocompletion...
;if active title contains any comma delimited strings below, then autocompletion will be deactivated for that window
excludedTitles = CodeQuickTester,Pulover's Macro Creator,GoToTilla,GoTo
;toggle one press autocomplete,i.e 'Tab' autocompletes with a space and 'Enter' will goto newline after autocomplete
onePressAutocomplete := True
;If autocomplete is enabled for all windows, text from last supported window will be used for autocompletion in an unsupported window where active input is detected
autocompleteAllWindows := True ;this refers to windows from which a wordlist can not be retrieved
;Input string will be replaced by suggested string,instead of completing missing strings. If true it's slower as input strings need to be removed before autocompletion.
caseCorrection := false
;Suggestions are ranked by Case,relative to input case.
caseSensitiveSuggesstions := True
;remove punctuation from matches.
removePunctuationFromMatches := false
;===================================== CONFIGURATION END ==================================================
;get version of this script - First Line of this script is reserved as a version header, so don't remove it!
If !A_IsCompiled
FileReadLine, thisScriptVersion, %A_ScriptFullPath%, 1
StringTrimLeft, thisScriptVersion, thisScriptVersion, 2
trayMsg =
(
Trigger Length: %triggerLength%
One Press Autocomplete: %onePressAutocomplete%
Autocomplete All Win.: %autocompleteAllWindows%
Case Correction: %caseCorrection%
Case Rank: %caseSensitiveSuggesstions%
Remove Punctuations: %removePunctuationFromMatches%
Excluded Processes:
%excludedProcessesList%
)
TrayTip, AutocompleteExistingText [@brutus_skywalker] %thisScriptVersion%, % StrReplace(StrReplace(trayMsg, " 1", " True"), " 0", " False") , , 1
;============================================================= AutoExecute Start ==================================================
;add excluded processes defined above to group
Loop, Parse, excludedProcessesList, `,
GroupAdd, excludedProcesses, ahk_exe %A_LoopField%
If caseSensitiveSuggesstions
StringCaseSense, On
DetectHiddenWindows, On
#MaxThreadsBuffer On
#MaxThreadsPerHotkey 1
Gosub, initialiseInputHotkeys
Gosub, updateWordList
SetTimer, updateWordList, 2500
SetTimer, autocompletePermissions, 100
SetTimer, sanityCheck, 250 ;monitors observed anomalies that occur as a result of unpredictable user behaviour or possible anomalous script behaviour.
Loop
GetInputString(word) ;accepts unknown characters not handled by hotkeys, this method isn't primary as it misses inputs in a large enough script.
;==================================================================================================================================
;------------------------------------------------------------------------------------
/*
NOTE: These are script anomalies that could happen due to variances in speed of user input. Adjusting the script to a specific input speed range,
is unreasonable,and it only results in either making it more or less responsive but not both across the wide range of user input speed, as such,
these simple checks serve to maintain that balance by correcting anomalies that occur at either input speed extreme.
*/
;------------------------------------------------------------------------------------
sanityCheck: ;checks for observed anomalies to make corrections.
;------------------------------------------------------------------------------------
SetBatchLines, -1
;------------------------------------------------------------------------------------
If (isListBoxActive AND !word){
GoSub, GuiClose
}
;------------------------------------------------------------------------------------
DetectHiddenWindows, off ;only cross check non-hidden windows,as listbox if not visible is always hidden
If (!isListBoxActive AND WinExist(listBoxTitle)){
GoSub, GuiClose
}Else If(isListBoxActive AND !WinExist(listBoxTitle)){
GoSub, GuiClose
}
DetectHiddenWindows, on
;------------------------------------------------------------------------------------
SetBatchLines, 10ms ;default
;------------------------------------------------------------------------------------
Return
;------------------------------------------------------------------------------------
;activates and deactivates hotkeys based on the compatibility/eclusion rules of/for active window.
autocompletePermissions:
If WinActive("ahk_group excludedProcesses"){ ;if excluded process,then suspend hotkeys
Suspend, on
isSuspended := True
}Else If(!GetTextFromControl() AND !autocompleteAllWindows){ ;if unsupported window and autocompleteAllWindows is disabled
Suspend, on
isSuspended := True
}Else If WinTitleContains(excludedTitles){
Suspend, on
isSuspended := True
}Else{
Suspend, off
isSuspended := False
}
Return
;'word' is the global variable that corresponds to currently being typed string
updateListBox:
SetBatchLines, -1
theseWords := WordsStartWithString(word,wordList,triggerLength) ;search current wordlist for matches of already typed string & output wordList, with second param containing minimum strLen to trigger match.
;ToolTip, % A_Index "`n" theseWords "`n" word, 0 ,0, 2
; ToolTip, % LB "-" StringsToPut(word, LB) "-" word
If theseWords
CreateUpdateListBox( theseWords ) ;use the wordlist from search result to generate an interactive ListBox
Else{
Gui, Show, Hide, %listBoxTitle% ;if no matches for currently input string Destroy listbox
isListBoxActive := false
}
SetBatchLines, 10ms ;default
return
;Create transparent Hotkeys for all relevant keys
initialiseInputHotkeys:
; Normal keys.
For Index, Key in StrSplit("abcdefghijklmnopqrstuvwxyz01234567890") {
Hotkey, % "~" Key, parseInputString
Hotkey, % "~+" Key, parseInputStringUpper
}
;End keys
endKeyList = Enter Tab Space `. `; `, `: `? `! `' `" `( `) `] `{ `} BackSpace Esc LButton RButton Left Right Pgdn Pgup End Home Ins Del NumpadLeft NumpadRight NumpadUp NumpadDown NumpadPgdn NumpadPgup NumpadHome NumpadEnd NumpadIns NumpadDel
For Index, Key in StrSplit(endKeyList, A_Space)
Hotkey, % "~" Key, endKeys
return
parseInputString:
StringTrimLeft, pressedHotkey, A_ThisHotkey, 1 ;To remove '~' from A_ThisHotkey
If (GetKeyState("Capslock", "T")) ; If Capslock is on, then send the capped key
StringUpper, pressedHotkey, pressedHotkey
word .= pressedHotkey
Gosub, updateListBox
return
parseInputStringUpper:
StringTrimLeft, pressedHotkey, A_ThisHotkey, 2 ;To remove '~' and '+' from A_ThisHotkey
StringUpper, pressedHotkey, pressedHotkey
word .= pressedHotkey
Gosub, updateListBox
return
endKeys:
Critical
;ToolTip, % A_ThisHotkey, 0,0,2 ;debug ToolTip
If (A_ThisHotkey = "~BackSpace"){
StringTrimRight, word, word, 1
Gosub, updateListBox
return
}
Critical, off
Gosub, GuiClose
Gosub, updateListBox
return
;=====================================
;Dynamic Wordlist Parsing label
;=====================================
updateWordList:
SetBatchLines, -1 ;go as fast as possible to reduce update delay
thisEditorText := GetTextFromControl()
If !thisEditorText{ ;to preseve wordlist from last supported window for windows from which a wordlist can not be retrieved.
SetBatchLines, 10ms ;default speed
supportedWindow := false ;signifies active window has no supported edit control
Return
}
supportedWindow:=true
;don't rebuild word list if text content of window hasn't changed
IfEqual, thisEditorText, %lastEditorText%
{
SetBatchLines, 10ms ;default speed
Return
}
SetTimer, updateWordList, off
While (GetAllKeysPressed() AND A_TimeIdlePhysical < 1000 OR isListBoxActive){ ;wait until user is done typing before updating the word list further
Sleep, 1000
}
SetTimer, updateWordList, on
;Get Wordlist
this_WordList := AlphaSortList(GetActiveWinWordList()) ;get & sort worldlist from active window text
If (!this_WordList){ ;if current window has no wordlist,retain old one.
SetBatchLines, 10ms ;default speed
Return
}
; ToolTip, % this_WordList, 400, 0, 3
WordList := this_WordList
; SoundBeep
lastEditorText := thisEditorText
SetBatchLines, 10ms ;default speed
Return
;=====================================
;ListBox control and interaction labels
;=====================================
;corresponds to an invisible button that allows interaction using Up/Down/Enter keys with listbox. Updates on pressing Enter.
ButtonInput:
Gui, Submit, NoHide
;ToolTip, %LB%
GoSub, GuiClose
Return
;updates with every change in selection of listbox element, 'LB' contains current selection.
CurrentSelection:
Gui, Submit, NoHide
; ToolTip, % LB "-" StringsToPut(word, LB) "-" word
Return
;moves listboxes to caret position with offset so no occlusion occurs,and destroys lisbox when window changes.
FollowCaret:
;ensures listbox is positioned consistently & predictably
CoordMode, caret, Screen
;move ListBox to caret position with an offset so it doesn't occlude text on caret position
;Keep ListBox from being generated outside the edges of the display...
; ToolTip, % W "`n" H "`n" A_CaretX + 20 "`n" A_CaretY + 25
If( A_CaretX + 20 + W < A_ScreenWidth AND A_CaretY + 25 + H < A_ScreenHeight){
WinMove, %listBoxTitle%, , % A_CaretX + 20, % A_CaretY + 25
}
Else{
If(A_CaretX + 20 + W > A_ScreenWidth){ ;if width is beyond display width
xMove := A_CaretX - 20 - W
}Else
xMove := A_CaretX + 20
If(A_CaretY + 25 + H > A_ScreenHeight){ ;if height is beyond display width
yMove := A_CaretY - 25 - H
}Else
yMove := A_CaretY + 25
WinMove, %listBoxTitle%, , % xMove, % yMove
}
If (!A_CaretX OR WinChanged()){ ;if no caret Destroy Gui
Gosub, GuiClose
}
If WinChanged()
Gosub, updateWordList
Return
GuiClose:
GuiEscape:
word := ""
Gui, Show, Hide, %listBoxTitle%
isListBoxActive := false
Return
;List Box Interaction Hotkeys - context sensitive to listbox,and send inputs sent directly to the listbox.
#If isListBoxActive ;if listbox exists and suggestions are active
Up::
Down::
Enter::
Tab::
Esc::
SetBatchLines, -1
Critical
IfEqual, A_ThisHotkey, Up
ControlSend, , {Up}, % listBoxTitle
IfEqual, A_ThisHotkey, Down
ControlSend, , {Down}, % listBoxTitle
IfEqual, A_ThisHotkey, Enter
{
ControlSend, , {Enter}, % listBoxTitle
If !caseCorrection{
If onePressAutocomplete
Put(StringsToPut(word,LB) . "`n") ;input remaining strings to Autocomplete and newline -as enter was pressed
Else
Put(StringsToPut(word,LB))
}Else{
inputStrLength := StrLen(word)
Send {BackSpace %inputStrLength%} ;backspace remove already input strings
If onePressAutocomplete
Put(LB . "`n") ;input current selection in full.
Else
Put(LB)
}
Gosub, GuiClose
}
IfEqual, A_ThisHotkey, Tab
{
ControlSend, , {Enter}, % listBoxTitle
If !caseCorrection{
If onePressAutocomplete
Put(StringsToPut(word,LB) . A_Space) ;input remaining strings to Autocomplete and space -as tab was pressed
Else
Put(StringsToPut(word,LB))
}Else{
inputStrLength := StrLen(word)
Send {BackSpace %inputStrLength%} ;backspace remove already input strings
If onePressAutocomplete
Put(LB . A_Space) ;input current selection in full.
Else
Put(LB)
}
Gosub, GuiClose
}
IfEqual, A_ThisHotkey, Esc
Gosub, GuiClose
Critical, off
SetBatchLines, 10ms ;default
Return
#If
;Autocomplete Search Engine
WordsStartWithString(string,ByRef wordList, stringLenToMatch:=1){
If ( StrLen(string) >= stringLenToMatch AND StrLen(string)){
Loop, Parse, wordList, `n
IfEqual, string, % SubStr(A_LoopField, 1, StrLen(string)) ;if current looped word starts with the string,then it's a match.
matches .= A_LoopField "`n"
IfEqual, A_StringCaseSense, off ;return matches if only a case insensitive matching was used,as it has all possible matches.
{
RemoveDuplicatesInList(matches, "`n")
Return, matches
}
Else ;append matches that were passed up for case insensitivity to above matches.
{
StringCaseSense, off
Loop, Parse, wordList, `n
IfEqual, string, % SubStr(A_LoopField, 1, StrLen(string)) ;if current looped word starts with the string,then it's a match.
{
StringCaseSense, on
matches .= A_LoopField "`n"
StringCaseSense, off
}
StringCaseSense, on
RemoveDuplicatesInList(matches, "`n")
Return, matches
}
}
}
;MsgBox, % StringsToPut("ar", "archemedis") ;will return 'chemedis'
StringsToPut(string, match){ ;takes strings already typed, actively matched string & determines what strings to append to complete typed strings.
Return, % SubStr(match, StrLen(string)+1, StrLen(match))
}
;Created listbox will dynamically resize itself to the width & height of provided WordList & Up/Down/Enter selection of list.
CreateUpdateListBox(WordList){ ;Verbose Commentary as this was my first ListBox, might be helpful to others as well....
static
Global listBoxTitle, isListBoxActive, W, H
listBoxTitle = WordListGUI
fontSize = 12
If guiInitialised{ ;if listbox already created,update it
;leave off the first | to add to listbox instead of replacing It's contents.
;Consider chosen delimiter here,i.e if Delimiter is line feed then add `n before new list to replace old list.
GuiControl,,LB,`n%WordList%
;get dimensions for updated list
W := LB_EX_CalcWidth(HLB)
H := LB_EX_CalcHeight(HLB)
;select the first entry on updated listBox
Control, Choose, 1, , %listBoxTitle%
;scale updated list to number of elements
GuiControl, Move, LB, h%H% w%W%
Gui, +AlwaysOnTop
Gui, Show, AutoSize NoActivate, %listBoxTitle%
isListBoxActive := true
Gosub, FollowCaret ;update ListBox position to caret position
Return
}
guiInitialised:=True ;signifies gui has already been created and need only be updated in future function calls.
;set font & size, ultimately determines gui size
Gui, Font, s%fontSize%, Courier New
;to set Delimiter used by GUI, default is pipe '|', any entry followed by double pipes will be default selection, 'red|blue||green', -blue will be default selection.
Gui, +Delimiter`n
;vLB -has current listbox selection, gCurrentSelection -triggers the label on new selection, Choose1 sets first entry as default selection.
;Extra Note: AltSubmit will Return Index of current listbox selection, instead of currently selected text.
;hwndHLB saves the handle to the listbox to a variable: 'HLB', for use with justme's functions.
Gui, Add, ListBox,x0 y0 -vscroll -hscroll Choose1 hwndHLB vLB gCurrentSelection, %WordList%
;determine suitable width and height for current list
W := LB_EX_CalcWidth(HLB)
H := LB_EX_CalcHeight(HLB)
;input button for Up/Down/Enter interaction, it triggers gButtonInput when Enter is pressed or DoubleClick is detected.
Gui, Add, Button,x0 y0 Default Hidden gButtonInput
;to remove the excess gui margins to the right and bottom
Gui, Margin, 0, 0
;...Remove titlebar & borders, ...Set as AlwaysOnTop, Set as LastFound window for succeding commands that act on a GUI/Window
Gui, -Caption +ToolWindow +AlwaysOnTop +LastFound -SysMenu
;set gui dimensions
GuiControl, Move, LB, w%W% h%H%
; Gui, Show
Gui,Show, AutoSize NoActivate X%A_ScreenWidth% Y%A_ScreenHeight%, %listBoxTitle% ;create gui offscreen, to avoid flickering and such.
isListBoxActive := true
;initialise current window as last window when checking for window change
WinChanged()
;initialise caret following, to place listbox at current caret position
Gosub, FollowCaret
SetTimer, FollowCaret, 100
lastWordList := WordList
}
WinChanged(){
static lastHwnd
If ( WinExist("A") != lastHwnd ){
lastHwnd := WinExist("A")
Return True
}
}
Put(ByRef string){ ;as an alternative to send,to instantly place a string. Using ByRef to get as best a performance as possible however negligible.
Global supportedWindow
ClipSaved := ClipboardAll
clipboard := ""
clipboard := string
clipwait
BlockInput, on
If supportedWindow
Send ^v
Else
Send, % string
BlockInput, off
Clipboard := ClipSaved
}
WinTitleContains(titles){ ;Returns true if active title contains strings defined in comma delimited param.
WinGetActiveTitle, activeTitle
Loop, Parse, titles, `,
IfInString, activeTitle, %A_LoopField%
Return, A_LoopField
}
;ListBox functions excerpt from the LBEX library by 'justme'
LB_EX_CalcWidth(HLB) { ; Calculates the width of the list box needed to show the whole content
; HLB - Handle to the ListBox
MaxW := 0
ControlGet, Items, List, , , % "ahk_id " . HLB
SendMessage, 0x0031, 0, 0, , % "ahk_id " . HLB ; WM_GETFONT
HFONT := ErrorLevel
HDC := DllCall("User32.dll\GetDC", "Ptr", HLB, "UPtr")
DllCall("Gdi32.dll\SelectObject", "Ptr", HDC, "Ptr", HFONT)
VarSetCapacity(SIZE, 8, 0)
Loop, Parse, Items, `n
{
Txt := A_LoopField
DllCall("Gdi32.dll\GetTextExtentPoint32", "Ptr", HDC, "Ptr", &Txt, "Int", StrLen(Txt), "Ptr", &Size)
If (W := NumGet(SIZE, 0, "Int")) > MaxW
MaxW := W
}
DllCall("User32.dll\ReleaseDC", "Ptr", HLB, "Ptr", HDC)
Return MaxW + 8 ; + 8 for the margins
}
LB_EX_CalcHeight(HLB) { ; Calculates the height of the list box needed to show the whole content
; HLB - Handle to the ListBox
Static LB_GETITEMHEIGHT := 0x01A1
Static LB_GETCOUNT := 0x018B
SendMessage, % LB_GETITEMHEIGHT, 0, 0, , % "ahk_id " . HLB
H := ErrorLevel
SendMessage, % LB_GETCOUNT, 0, 0, , % "ahk_id " . HLB
Return (H * ErrorLevel) + 8 ; + 8 for the margins
}
;Dynamic WordList Parsing functions
GetActiveWinWordList(){
Static
text := GetTextFromControl()
If !text ;if no text can be retrieved from a control get all text in window
WinGetText, text, A
;rebuild WordList only if text in editor has changed
IfEqual, text, %lastText%
Return lastWordList
lastText := text
;redact all punctuations if options was set to true
Global removePunctuationFromMatches
If (removePunctuationFromMatches){ ;doing it this way because i couldn't figure it out with RegEx
punctuation = `. `; `, `: `? `! `' `" `( `) `[ `] `{ `} `/ `\ `< `>
Loop, Parse, punctuation, %A_Space%
StringReplace, text, text, %A_LoopField%, `n, ALL
}
StringReplace, text, text, `,, `n, ALL
StringReplace, text, text, `r`n, `n, ALL
StringReplace, text, text, %A_Space%, `n, ALL
StringReplace, text, text, %A_Tab%, `n, ALL
;remove white spaces
wordListText := RegExReplace(text , "(^|\R)\K\s+")
;rebuild wordlist using only strings longer than 3 characters
Wordlist:=""
Loop, Parse, wordListText, `n
If StrLen(A_LoopField) >= 3 ;strings of over 3 characters
IfNotInString, Wordlist, %A_Space%%A_LoopField%%A_Space% ;avoid repeated strings
Wordlist.= " " A_LoopField " " "`n" ;add spaces on either side so that words in other word can easily be added to list
StringReplace, Wordlist, Wordlist, %A_Space%, , ALL ;spaces are no longer needed as list is done building
lastWordList := WordList
Return WordList
}
GetTextFromControl(){
WinGet, controlList, ControlList, A
WinGet, ProcName , ProcessName, A
if regexmatch(ProcName, "i)(?:Sc(?:\d+|iTE))|(?:notepad(?:\+\+|2)).exe")
ControlGetText, text ,Scintilla1, A
else if InStr(controlList, "Edit3")
ControlGetText, text ,Edit3, A
else if InStr(controlList, "Edit1")
ControlGetText, text ,Edit1, A
else if InStr(controlList, "TConTEXTSynEdit1")
ControlGetText, text ,TConTEXTSynEdit1, A
else if InStr(controlList, "RICHEDIT50W1")
ControlGetText, text ,RICHEDIT50W1, A
else if InStr(controlList, "Scintilla1")
ControlGetText, text ,Scintilla1, A
return (text ? text : "")
}
AlphaSortList(ByRef list){
StringSplit, line, list, `n,`r
Loop, parse, list, `n, `r
data1 .= (data1?"`n":"") A_LoopField "|" A_Index
data1 := RegExReplace(data1, "[ `,“”\(\)\/`:]")
sort, data1
loop, parse, data1, `n,`r
l := RegExReplace(A_LoopField, ".*\|") , out .= (out?"`n":"") line%l%
Return out
}
; returns a variable containing names of all keys pressed down currently.
; Pass "L" (default) to check only the logical state of the keys, and "P" to check the physical state
GetAllKeysPressed(mode = "L") {
SetBatchLines, -1
keys = ``|1|2|3|4|5|6|7|8|9|0|-|=|[|]\|;|'|,|.|/|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|Esc|Tab|CapsLock|LShift|RShift|LCtrl|RCtrl|LWin|RWin|LAlt|RAlt|Space|AppsKey|Up|Down|Left|Right|Enter|BackSpace|Delete|Home|End|PGUP|PGDN|PrintScreen|ScrollLock|Pause|Insert|NumLock|F1|F2|F3|F4|F5|F6|F7|F8|F9|F10|F11|F12|F13|F14|F15|F16|F17|F18|F19|F20
; '|' isn't a key itself (with '\' being the "actual" key), so okay to use is as a delimiter
Loop Parse, keys, |
{
key = %A_LoopField%
isDown := GetKeyState(key, mode)
if isDown
pressed .= key
}
SetBatchLines, 10ms
return pressed
}
RemoveDuplicatesInList(ByRef list, delimiter){
caseSenseState := A_StringCaseSense
StringCaseSense, on
;create a padded list so that words in other words aren't matched
;the logic is, pseduocode: IfInString, " HelloWorld "," Hello " will not return true,
;where as IfInString, "HelloWorld", "Hello" will return true. This method is more effective in lists.
Loop, Parse, list, %delimiter%
paddedList .= A_Space . A_LoopField . A_Space . "`n"
;rebuild list minus duplicates
list := ""
Loop, Parse, paddedList, `n
IfNotInString, list, %A_LoopField%
list .= A_LoopField "`n"
StringReplace, list, list, %A_Space%, , ALL ;remove all space, as padding is no longer needed
StringCaseSense, %caseSenseState%
}
;============================================================================================
/*
For Broader Keyboard Compatibility, an alternate Input capture scheme that appends to the main input string.
It only accepts inputs that aren't covered by hotkeys to prevent duplication of inputs,
*/
;============================================================================================
GetInputString(ByRef word){
hotkeys = a A b B c C d D e E f F g G h H i I j J k K l L m M n N o O p P q Q r R s S t T u U v V w W x X y Y z Z 0 1 2 3 4 5 6 7 8 9 0 Enter Tab Space `. `; `, `: `? `! `' `" `( `) `] `{ `} BackSpace Esc
;Get one key at a time
Input, chr, L1 V,{enter}{space}.;`,:¿?¡!'"()]{}{bs}{esc}
SetBatchLines, -1
Loop, Parse, hotkeys, %A_Space%
IfEqual, chr, %A_LoopField%
atLeastOneHotKeyMatches := true
Global isSuspended
If (!atLeastOneHotKeyMatches AND !isSuspended){ ;if no hotkey matches currently input character add it to currently input word
word = %word%%chr%
}
SetBatchLines, 10ms ;default
}
Below are the components that comprise the above script, i'm posting them such that others may have an easier start in similar scripts or just code to cannibalise if you so wish...cheers.
Spoiler
Legacy Tooltip version, in my OP is also down here for any one who might want it,as it's less intrusive being a tooltip and all. I Run both personally, i run the tooltip version on all my Virtual Machines,as it works across the board with no hiccups.
Based on variations on THIS post of rajat's Intellisense script modifications.
; DOUBLE Press 1 to 0 keys to autocomplete the word upon suggestion
; (0 will match suggestion 10)
Code: Select all
/*
CONFIGURATION NOTE:
Tooltip position is defined on line 163.
Input method is defined on line 330,It's currently an alternation between Clipboard"using the Put() function" or sending it raw based on context.
Update rate defined on line 53.
*/
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
#SingleInstance, force
SetKeyDelay, 0
CoordMode, ToolTip, Relative
AutoTrim, Off
;_______________________________________
; CONFIGURATIONS
; Editor Window Recognition
; (make it blank to make the script seek all windows)
ETitle =
;Minimum word length to make a guess
WLen = 3
keyagain=
key=
clearword=1
;Gosub,clearallvars ; clean vars from start
; Press 1 to 0 keys to autocomplete the word upon suggestion
; (0 will match suggestion 10)
;_______________________________________
Wordlist= ;for debug/testing
(
this
is
a
baseline
wordlist
)
SetTimer, updateWordList, 10000
Gosub, updateWordList
;reads all words from var
Loop, Parse, Wordlist, `n
{
tosend = %a_loopfield%
cmd%a_index% = %toSend%
}
SetTimer, Winchanged, 100
Loop
{
;Editor window check
WinGetActiveTitle, ATitle
WinGet, A_id, ID, %ATitle%
IfNotInString, ATitle, %ETitle%
{
ToolTip
Setenv, Word,
sleep, 500
Continue
}
;Get one key at a time
Input, chr, L1 V,{enter}{space}.;`,:¿?¡!'"()]{}{bs}{esc}
EndKey = %errorlevel%
; If active window has different window ID from before the input, blank word
; (well, assign the number pressed to the word)
WinGetActiveTitle, ATitle
WinGet, A_id2, ID, %ATitle%
IfNotEqual, A_id, %A_id2%
{
Gosub,clearallvars
Setenv, Word, %chr%
Continue
}
;Blanks word reserve
ifequal, EndKey, Endkey:Enter, Gosub,clearallvars
ifequal, EndKey, Endkey:Escape, Gosub,clearallvars
ifequal, EndKey, Endkey:Space, Gosub,clearallvars
ifequal, EndKey, Endkey:`,, Gosub,clearallvars
ifequal, EndKey, Endkey:., Gosub,clearallvars
ifequal, EndKey, Endkey:`:, Gosub,clearallvars
ifequal, EndKey, Endkey:`;, Gosub,clearallvars
ifequal, EndKey, Endkey:!, Gosub,clearallvars
ifequal, EndKey, Endkey:¡, Gosub,clearallvars
ifequal, EndKey, Endkey:?, Gosub,clearallvars
ifequal, EndKey, Endkey:¿, Gosub,clearallvars
ifequal, EndKey, Endkey:", Gosub,clearallvars ;" comment to normalise syntax highliting
ifequal, EndKey, Endkey:', Gosub,clearallvars
ifequal, EndKey, Endkey:(, Gosub,clearallvars
ifequal, EndKey, Endkey:), Gosub,clearallvars
; ifequal, EndKey, Endkey:[, Gosub,clearallvars -- this was causing problem number 1
ifequal, EndKey, Endkey:], Gosub,clearallvars
ifequal, EndKey, Endkey:{, Gosub,clearallvars
ifequal, EndKey, Endkey:}, Gosub,clearallvars
;Backspace clears last letter
ifequal, EndKey, Endkey:BackSpace, StringTrimRight, Word, Word, 1
ifnotequal, EndKey, Endkey:BackSpace, Setenv, Word, %word%%chr%
;Wait till minimum letters
StringLen, len, Word
IfLess, len, %wlen%
{
ToolTip
Continue
}
;Match part-word with command
Num =
Match =
singlematch = 0
number = 0
Loop
{
IfEqual, cmd%a_index%,, Break
StringLen, chars, Word
StringLeft, strippedcmd, cmd%a_index%, %chars%
StringLeft, strippedword, Word, %chars%
ifequal, strippedcmd, %strippedword%
{
num = %a_index%
number ++
; Create list of matches
StringTrimLeft, singlematch, cmd%num%, 0
match = %match%%number%. %singlematch%`n
; Map singlematch with corresponding cmd
singlematch%number%=cmd%num%
Continue
}
}
;If no match then clear Tip
IfEqual, Num,
{
clearword=0
Gosub,clearallvars
Continue
}
;Show matched command
StringTrimRight, match, match, 1 ; Get rid of the last linefeed
IfNotEqual, Word,,ToolTip, %match%, A_CaretX + 200, A_CaretY + 40
}
; Timed function to detect change of focus (and remove tooltip when changing active window)
Winchanged:
WinGetActiveTitle, ATitle
WinGet, A_id3, ID, %ATitle%
IfNotEqual, A_id, %A_id3%
{
ToolTip
Gosub, updateWordList ;update word list anytime window changes
}
Return
; Key definitions for autocomplete (0 to 9)
#MaxThreadsPerHotkey 1
$1::
$2::
$3::
$4::
$5::
$6::
$7::
$8::
$9::
$0::
Gosub, checkword
Return
; If hotkey was pressed, check wether there's a match going on and send it, otherwise send the number(s) typed
checkword:
SetBatchLines, -1
clearword=1
Suspend, on ; Suspend hotkeys so that they don't interfere with the second press
key := A_ThisHotkey
StringTrimLeft, key, key, 1
; If active window has different window ID from before the input, blank word
; (well, assign the number pressed to the word)
WinGetActiveTitle, ATitle
WinGet, A_id2, ID, %ATitle%
IfNotEqual, A_id, %A_id2%
{
if key =10
key = 0
Send,%key%
Gosub,clearallvars
Suspend, off
Return
}
if word= ; only continue if word is not empty
{
if key =10
key = 0
Send,%key%
Setenv, Word, %key%
clearword=0
Gosub,clearallvars
Suspend, off
Return
}
ifequal, singlematch%key%, ; only continue singlematch is not empty
{
if key =10
key = 0
Send,%key%
Setenv, Word, %word%%key%
clearword=0
Gosub,clearallvars
Suspend, off
Return
}
; 2nd press to confirm replacement
Input, keyagain, L1 I T0.5, 1234567890
; msgbox, ErrorLevel=%ErrorLevel% ; UNCOMMENT FOR TESTING 2ND PROBLEM DISCUSSED IN POST
; If there is a timeout, abort replacement, send key and return
IfEqual, ErrorLevel, Timeout
{
if key =10
key = 0
Send, %key%
Setenv, Word, %word%%key%
clearword=0
Gosub,clearallvars
Suspend, off
Return
}
; Make sure it's an EndKey, otherwise abort replacement, send key and return
IfNotInString, ErrorLevel, EndKey:
{
if key =10
key = 0
Send, %key%%keyagain%
Setenv, Word, %word%%key%%keyagain%
clearword=0
Gosub,clearallvars
Suspend, off
Return
}
; If the 2nd key is NOT the same 1st trigger key, abort replacement and send keys
if key =10
key = 0
IfNotInString,ErrorLevel, %key%
{
StringTrimLeft, keyagain, ErrorLevel, 7
Send, %key%%keyagain%
Setenv, Word, %word%%key%%keyagain%
clearword=0
Gosub,clearallvars
Suspend, off
Return
}
; SEND THE WORD!
if key =0
key = 10
StringTrimLeft, lastone, singlematch%key%, 0 ; This is because i can't get %singlematch%%key%
StringTrimLeft, sending, %lastone%, 0 ; to work in this line
StringLen, len, Word
Send, {BS %len%} ; First do the backpaces
If A_CaretX ;if there is a caret then this is an editable window so paste into it
Put(sending . " ") ; Paste it instead - clipboard will be backed up
Else ;if there is no caret send it raw as keystrokes
SendRaw, %sending% ; Then send word (Raw because we want the string exactly as in wordlist.txt)
Gosub,clearallvars
Suspend, off
SetBatchLines, 10ms
Return
; This is to blank all vars related to matches, tooltip and (optionally) word
clearallvars:
SetBatchLines, -1
Ifequal,clearword,1,Setenv,word,
ToolTip
; Clear all singlematches
Loop, 10
{
singlematch%a_index% =
}
sending =
key=
match=
clearword=1
SetBatchLines, 10ms ;default
Return
updateWordList:
SetBatchLines, -1 ;go as fast as possible to reduce update delay
thisEditorText := GetTextFromControl()
;don't rebuild word list if text content of window hasn't changed
IfEqual, thisEditorText, %lastEditorText%
{
SetBatchLines, 10ms ;default speed
Return
}
SetTimer, updateWordList, off
While (GetAllKeysPressed() AND A_TimeIdlePhysical < 5000){ ;wait until user is done typing before updating the word list further
Sleep, 1000
}
SetTimer, updateWordList, on
;Get Wordlist
this_WordList := AlphaSortList(GetActiveWinWordList()) ;get & sort worldlist from active window text
If (!this_WordList){ ;if current window has no wordlist,retain old one.
SetBatchLines, 10ms ;default speed
Return
}
;rebuild wordlist to vars
Loop, Parse, this_WordList, `n
{
tosend = %a_loopfield%
cmd%a_index% = %toSend%
}
; ToolTip, %WordList%
; SoundBeep
lastEditorText := thisEditorText
SetBatchLines, 10ms ;default speed
Return
Put(string){ ;as an alternative to send,to instantly place a string
ClipSaved := ClipboardAll
clipboard := ""
clipboard := string
clipwait
BlockInput, on
Send ^v
BlockInput, off
Clipboard := ClipSaved
}
GetActiveWinWordList(){
Global lastText
Global lastWordList
text := GetTextFromControl()
If !text ;if no text can be retrieved from a control get all text in window
WinGetText, text, A
;rebuild WordList only if text in editor has changed
IfEqual, text, %lastText%
Return lastWordList
lastText := text
StringReplace, text, text, `,, `n, ALL
StringReplace, text, text, `r`n, `n, ALL
StringReplace, text, text, %A_Space%, `n, ALL
StringReplace, text, text, %A_Tab%, `n, ALL
;remove white spaces
wordListText := RegExReplace(text , "(^|\R)\K\s+")
;rebuild wordlist using only strings longer than 3 characters
Wordlist:=""
Loop, Parse, wordListText, `n
If StrLen(A_LoopField) >= 3 ;strings of over 3 characters
IfNotInString, Wordlist, %A_Space%%A_LoopField%%A_Space% ;avoid repeated strings
Wordlist.= " " A_LoopField " " "`n" ;add spaces on either side so that words in other word can easily be added to list
StringReplace, Wordlist, Wordlist, %A_Space%, , ALL ;spaces are no longer needed as list is done building
lastWordList := WordList
Return WordList
}
GetTextFromControl(){
WinGet, controlList, ControlList, A
WinGet, ProcName , ProcessName, A
if regexmatch(ProcName, "i)(?:Sc(?:\d+|iTE))|(?:notepad(?:\+\+|2)).exe")
ControlGetText, text ,Scintilla1, A
else if InStr(controlList, "Edit3")
ControlGetText, text ,Edit3, A
else if InStr(controlList, "Edit1")
ControlGetText, text ,Edit1, A
else if InStr(controlList, "TConTEXTSynEdit1")
ControlGetText, text ,TConTEXTSynEdit1, A
else if InStr(controlList, "RICHEDIT50W1")
ControlGetText, text ,RICHEDIT50W1, A
else if InStr(controlList, "Scintilla1")
ControlGetText, text ,Scintilla1, A
return (text ? text : "")
}
AlphaSortList(list){
StringSplit, line, list, `n,`r
Loop, parse, list, `n, `r
data1 .= (data1?"`n":"") A_LoopField "|" A_Index
data1 := RegExReplace(data1, "[ `,“”\(\)\/`:]")
sort, data1
loop, parse, data1, `n,`r
l := RegExReplace(A_LoopField, ".*\|") , out .= (out?"`n":"") line%l%
Return out
}
; returns a variable containing names of all keys pressed down currently.
; Pass "L" (default) to check only the logical state of the keys, and "P" to check the physical state
GetAllKeysPressed(mode = "L") {
SetBatchLines, -1
keys = ``|1|2|3|4|5|6|7|8|9|0|-|=|[|]\|;|'|,|.|/|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|Esc|Tab|CapsLock|LShift|RShift|LCtrl|RCtrl|LWin|RWin|LAlt|RAlt|Space|AppsKey|Up|Down|Left|Right|Enter|BackSpace|Delete|Home|End|PGUP|PGDN|PrintScreen|ScrollLock|Pause|Insert|NumLock|F1|F2|F3|F4|F5|F6|F7|F8|F9|F10|F11|F12|F13|F14|F15|F16|F17|F18|F19|F20
; '|' isn't a key itself (with '\' being the "actual" key), so okay to use is as a delimiter
Loop Parse, keys, |
{
key = %A_LoopField%
isDown := GetKeyState(key, mode)
if isDown
pressed .= key
}
SetBatchLines, 10ms
return pressed
}