AutoCorrect for v2

Post your working scripts, libraries and tools.
Jasonosaj
Posts: 51
Joined: 02 Feb 2022, 15:02
Location: California

Re: AutoCorrect for v2

Post by Jasonosaj » 20 Feb 2024, 14:24

Descolada wrote:
20 Feb 2024, 00:01
@Jasonosaj AFAIK there currently is no way to get the case of the hotstring trigger, which is why I created a pull request that implements a way to access the hotstring recognizer, which could then be used to extract the trigger along with its case.
You're awesome. Wish I had just asked!

Descolada
Posts: 1141
Joined: 23 Dec 2021, 02:30

Re: AutoCorrect for v2

Post by Descolada » 21 Feb 2024, 00:02

kunkel321 wrote:
20 Feb 2024, 08:19
Would it be helpful if Jason and I add comments to it, stating our support? Or is that just annoying to Lexikos?
I can't speak for him, but if you want voice your support to implement a feature which gives access to the case conformity of hotstring triggers, then why not. Though you probably shouldn't endorse my specific implementation, because there might be better alternatives, or perhaps messing with the recognizer has some side-effects I'm not aware of.

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 28 Feb 2024, 18:22

@Descolada or @Jasonosaj, Is there a need for a version of the above HotString Helper 2.0 that's made for _HS() functions? Or not really?
ste(phen|ve) kunkel

Jasonosaj
Posts: 51
Joined: 02 Feb 2022, 15:02
Location: California

Re: AutoCorrect for v2

Post by Jasonosaj » 28 Feb 2024, 18:24

@kunkel321 I've been hacking away at the latest version and thought you might like to take a look. [url]https://gist.githubusercontent.com/Jason-K/0ad62a0c203b48845c0967e2e532dc54/raw/bdcfa47a2bc22f14f603adc592ef1457b5ffa7fd/February%2028%2C%202024.md[\url]

Primary changes have to do with:
1. I am no longer passing A_ variables as params
2. I have added a case param to f(), which handles those situations in which the output will always need to be a specific case (e.g., NATO, ORIF, etc.)
3. I have modified the way that data is passed around at least some of the functions, leaning on objects to pass multiple variables without resorting to globals
4. I have modified the verification and the appendit functions to identify all conflicting strings (not just the first one) and give the user the option to either comment out the conflicts or supersede them by writing the new string immediately before the first conflict (then adding a datestamp to the comment in case one needs to reverse this process.

We still have the problem with accessing the case of the input text, but these seem to be useful changes on my end. Also a useful exercise in writing in v2. NOT as easy as v1.

Descolada
Posts: 1141
Joined: 23 Dec 2021, 02:30

Re: AutoCorrect for v2

Post by Descolada » 29 Feb 2024, 01:47

@kunkel321 although I am not personally using this Autocorrect script, another user @someguyinKC is using _HS and is asking about AutoCorrect support. I'm not very familiar with your f function, but I think it also supports case conforming and includes a logging function? The main benefit of _HS at the moment is that it supports most of the hotstring options and different send modes (including custom ones, such as someguyinKC using Clip to send text). This means you could keep only the f function if it supported those things as well, I think?

Btw, I have two suggestions:
1) Constantly logging to a file is time- and resource-intensive, especially if the user is constantly using autocorrect. I would recommend instead keeping the log in a temporary variable and setting a timer for something like 5 minutes (and also OnExit), at which point the buffer would be written into the file.
2) I think the case conformity problem could be solved by using a constantly running InputHook (without the input buffering). You could start the hook on script start and once a autocorrect hotstring is activated, just read the last characters from the hook to get the case.

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 29 Feb 2024, 09:29

@Jasonosaj This is awesome! I still need to study it more, but I definitely like how you are relying more on function-call parameters, and less on global variables. I cringe every time I add another global variable, because there are so many, and I know it's not best practice to over-use them. That said... I've already added more since posting the last zip-- LOL.

@Descolada I think I will see about adding support for _HS() formatted hotstrings. It's worth noting that the HotString Helper 2.0 (hh2) code and the f() function code are totally separate (though in the same ahk file). So having a "_HS() version of hh2" doesn't involve changing _HS nor f() in any way. Adding support should be easy. The code to add "_HS" rather than "f()" is a single line of code. The only thing really that will need to be changed is that _HS uses the second parameter for holding certain hotstring options. That will need to be accommodated.

Also... Good point about the problem of constant logging. Thanks for pointing that out. For a couple of years, I've been using a laptop with dual solid state drives, so it's not an issue, but if a person is using a spinning hard disc, then the constant logging must get annoying. f() doesn't really support case conformation by the way. Though the f() and the _HS() functions would both (sort of) support an initial capital if/when the entire trigger was not backspaced.

EDIT: fyi @Jasonosaj here is the latest version of hh2. I had already expanded on the validation function a bit. And the validation "msgbox" is now a big (easy to see) colorful gui window. The Exam button is now multi-functional. Right-click (or shift+left click) on it for the "control pane" which has some links to related tool.

fyi I anyone tries the below code, they'll need the subfolder and wordlist that are in the zip, attached on page two of this thread... Here viewtopic.php?f=83&t=120220&start=20#p559328
EDIT again 2:30pm PST:
- r-click on Exam button now toggles more smartly.
- validity messages in "big MsgBox" are now selectable. ;)

Code: Select all

#SingleInstance
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode("RegEx")
#Requires AutoHotkey v2+

;===============================================================================
;            			Hotstring Helper 2.0
;          Hotkey: Win + H | By: Kunkel321 | Version: 2-28-2024
; https://www.autohotkey.com/boards/viewtopic.php?f=6&t=114688
; A version of Hotstring Helper that will support block multi-line replacements and 
; allow user to examine hotstring for multi-word matches. The "Examine/Analyze" 
; pop-down part of the form is based on the WAG tool here
; https://www.autohotkey.com/boards/viewtopic.php?f=83&t=120377
; Customization options are below, near top of code.
; Please get a copy of AutoHotkey.exe (v2) and rename it to match the name of this
; script file, so that the .exe and the .ahk have the same name, in the same folder.
; DO NOT COMPILE, or the Append command won't work. The Gui stays in RAM, but gets
; repopulated upon hotkey press. HotStrings will be appended (added) by the
; script at the bottom. Shift+Append saves to clipboard instead of appending. 
; This tool is intended to be embedded in your AutoCorrect list.
;===============================================================================

;==Change=color=of=Hotstring=Helper=form=as=desired===========================
GuiColor := "F5F5DC" ; "F0F8FF" is light blue. Tip: Use "Default" for Windows default.
FontColor := "003366" ; "003366" is dark blue. Tip: Use "Default" for Windows default.

; ===Change=Settings=for=Big=Validity=Dialog=Message=Box========================
myGreen := 'c1D7C08' ; light green 'cB5FFA4' (for use with dark backgrounds.)
myRed := 'cB90012' ; light red 'cFFB2AD'
myBigFont := 's13'

;==Change=Hotstring=Helper=Activation=Hotkey=as=desired=========================
hh_Hotkey := "#h" ; The activation hotkey-combo (not string) is Win+h. 

;==Change=title=of=Hotstring=Helper=form=as=desired=============================
hhFormName := "HotString Helper 2.0" ; The name at the top of the form. Change here, if desired.

; ======Change=size=of=GUI=when="Make Bigger"=is=invoked========================
HeightSizeIncrease := 300 ; Numbers, not 'strings,' so no quotation marks. 
WidthSizeIncrease := 400

;====Assign=symbols=for="Show Symb"=button======================================
myPilcrow := "¶"    ; Okay to change symbols if desired.
myDot := "• "       ; adding a space (optional) allows more natural wrapping.
myTab := "⟹ "      ; adding a space (optional) allows more natural wrapping.

;===Change=options=for=MULTI=word=entry=options=and=trigger=strings=as=desired==
; These are the defaults for "acronym" based boiler plate template trigger strings. 
DefaultBoilerPlateOpts := ""  ; PreEnter these multi-word hotstring options; "*" = end char not needed, etc.
myPrefix := ";"        ; Optional character that you want suggested at the beginning of each hotstring.
addFirstLetters := 5   ; Add first letter of this many words. (5 recommended; 0 = don't use feature.)
tooSmallLen := 2       ; Only first letters from words longer than this. (Moot if addFirstLetters = 0)
mySuffix := ""         ; An empty string "" means don't use feature.

;===============Change=options=AUTOCORRECT=words=as=desired=====================
; PreEnter these (single-word) autocorrect options; "T" = raw text mode, etc.
DefaultAutoCorrectOpts := "*" ; An empty string "" means don't use feature.

;=====List=of=words=use=for=examination=lookup==================================
WordListFile := 'GitHubComboList249k.txt' ; Mostly from github: Copyright (c) 2020 Wordnik
; WordListFile := 'wlist_match6.txt' ; From https://www.keithv.com/software/wlist/

;=====Other=Settings============================================================
; Add "Fixes X words, but misspells Y" to the end of autocorrect items. 
; 1 = Yes, 0 = No. Multi-line Continuation Section items are never auto-commented.
AutoCommentFixesAndMisspells := 1

;====Window=specific=hotkeys====================================================
; These can be edited... Cautiously. 
#HotIf WinActive(hhFormName) ; Allows window-specific hotkeys.
$Enter:: ; When Enter is pressed, but only in this GUI. "$" prevents accidental Enter key loop.
{ 	If (hh['SymTog'].text = "Hide Symb")
		return ; If 'Show symbols' is active, do nothing.
	Else if ReplaceString.Focused {
		Send("{Enter}") ; Just normal typing; Enter yields Enter key press.
		Return
	}
	Else hhButtonAppend() ; Replacement box not focused, so press Append button.
}
+Left:: ; Shift+Left: Got to trigger, move cursor far left.
{	TriggerString.Focus()
		Send "{Home}"
}
Esc::
{ 	hh.Hide()
	A_Clipboard := ClipboardOld
}
^z:: GoUndo() ; Undo last 'word exam' trims, one at a time.
^+z:: GoReStart() ; Put the whole trigger and replacement back (restart).
^Up:: 		; Ctrl+Up Arrow, or 
^WheelUp::	; Ctrl+Mouse Wheel Up to increase font size (toggle, not zoom.)
{	MyDefaultOpts.SetFont('s15')  ; sets at 15
	TriggerString.SetFont('s15')
	ReplaceString.SetFont('s15')
}
^Down:: 		; Ctrl+Down Arrow, or 
^WheelDown:: 	; Ctrl+Mouse Wheel Down to put font size back.
{	MyDefaultOpts.SetFont('s11')  ; sets back at 11
	TriggerString.SetFont('s11')
	ReplaceString.SetFont('s11')
}
#HotIf ; Turn off window-specific behavior.


; Make sure word list is there. Change name of word list subfolder, if desired. 
WordListPath := A_ScriptDir '\WordListsForHH\' WordListFile
If not FileExist(WordListPath)
	MsgBox("This error means that the big list of comparison words at:`n" . WordListPath . 
	"`nwas not found.`n`nTherefore the 'Exam' button of the Hotstring Helper tool won't work.")
SplitPath WordListPath, &WordListName ; Extract just the name of the file.

;===== Main Graphical User Interface (GUI) is built here =======================
hh := Gui('', hhFormName)
hh.Opt("-MinimizeBox +alwaysOnTop")
hh.BackColor := GuiColor
FontColor := FontColor != "" ? "c" . FontColor : ""
hh.SetFont("s11 " . FontColor)
hFactor := 0, wFactor := 0 ; Don't change size here. 
; -----  Trigger string parts ----
hh.AddText('y4 w30', 'Options')
(TrigLbl := hh.AddText('x+40 w250', 'Trigger String'))
(MyDefaultOpts := hh.AddEdit('yp+20 xm+2 w70 h24'))
(TriggerString := hh.AddEdit('x+18 w' . wFactor + 280, '')).OnEvent('Change', TriggerChanged)
; ----- Replacement string parts ----
hh.AddText('xm', 'Replacement')
hh.SetFont('s9')
hh.AddButton('vSizeTog x+75 yp-5 h8 +notab', 'Make Bigger').OnEvent("Click", TogSize)
hh.AddButton('vSymTog x+5 h8 +notab', '+ Symbols').OnEvent("Click", TogSym)
hh.SetFont('s11')
(ReplaceString := hh.AddEdit('vReplaceString +Wrap y+1 xs h' . hFactor + 100 . ' w' . wFactor + 370, '')).OnEvent('Change', GoFilter)
; ---- Below Replacement ----
ComLbl := hh.AddText('xm y' . hFactor + 182, 'Comment')
(ChkFunc := hh.AddCheckbox('vFunc, x+70 y' . hFactor + 182, 'Make Function')).onEvent('click', FormAsFunc)
ChkFunc.Value := 1 ; 'Make Function' box checked by default?  1 = checked.  
hh.SetFont("s11 cGreen")
ComStr := hh.AddEdit('vComStr xs y' . hFactor + 200 . ' w' . wFactor + 370)
hh.SetFont("s11 " . FontColor)
; ---- Buttons ----
(ButApp := hh.AddButton('xm y' . hFactor + 234, 'Append')).OnEvent("Click", hhButtonAppend)
(ButCheck := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Check')).OnEvent("Click", hhButtonCheck)
(ButExam := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Exam'))
ButExam.OnEvent("Click", hhButtonExam)
ButExam.OnEvent("ContextMenu", subFuncExamControl)
(ButSpell := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Spell')).OnEvent("Click", hhButtonSpell)
(ButOpen := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Open')).OnEvent("Click", hhButtonOpen)
(ButCancel := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Cancel')).OnEvent("Click", hhButtonCancel)
hh.OnEvent("Close", hhButtonCancel)
; ============== Bottom (toggling) "Exam Pane" part of GUI =====================
; ---- delta string ----
hh.SetFont('s10')
(ButLTrim := hh.AddButton('vbutLtrim xm h50  w' . (wFactor+182/6), '>>')).onEvent('click', GoLTrim)
hh.SetFont('s14')
(TxtTypo := hh.AddText('vTypoLabel -wrap +center cBlue x+1 w' . (wFactor+182*5/3), hhFormName))
hh.SetFont('s10')
(ButRTrim := hh.AddButton('vbutRtrim x+1 h50 w' . (wFactor+182/6), '<<')).onEvent('click', GoRTrim)
; ---- radio buttons -----
hh.SetFont('s11')
(RadBeg := hh.AddRadio('vBegRadio y+-18 x' . (wFactor+182/3), '&Beginnings')).onEvent('click', GoFilter)
(RadMid := hh.AddRadio('vMidRadio x+5', '&Middles')).onEvent('click', GoMidRadio)
(RadEnd := hh.AddRadio('vEndRadio x+5', '&Endings')).onEvent('click', GoFilter)
; ---- bottom buttons -----
(ButUndo := hh.AddButton('xm y+3 h26 w' . (wFactor+182*2), "Undo (+Reset)")).OnEvent('Click', GoUndo)
ButUndo.Enabled := false
; ---- results lists -----
hh.SetFont('s12')
(TxtTLable := hh.AddText('vTrigLabel center y+4 h25 xm w' . wFactor+182, 'Misspells'))
(TxtRLable := hh.AddText('vReplLabel center h25 x+5 w' . wFactor+182, 'Fixes'))
(EdtTMatches := hh.AddEdit('vTrigMatches y+1 xm h' . hFactor+300 . ' w' . wFactor+182,))
(EdtRMatches := hh.AddEdit('vReplMatches x+5 h' . hFactor+300 . ' w' . wFactor+182,))
; ---- word list file ----
hh.SetFont('bold s10')
(TxtWordList := hh.AddText('vWordList center xm y+1 h14 w' . wFactor*2+364 , WordListName)).OnEvent('DoubleClick', ChangeWordList)
ShowHideButtonExam(Visibility := False) ; Hides bottom part of GUI as default. 
; ============== Bottom (toggling) "Control Pane" part of GUI =====================
(TxtCtrlLbl1 := hh.AddText(' center cBlue ym+270 h25 xm w' . wFactor+370, 'Secret Control Panel!'))
hh.SetFont('s10')
(butRunAcLog := hh.AddButton('  y+5 h25 xm w' . wFactor+370, 'Open AutoCorrection Log'))
butRunAcLog.OnEvent("click", (*) => ControlPaneRuns("butRunAcLog"))
(butRunMcLog := hh.AddButton('  y+5 h25 xm w' . wFactor+370, 'Open Manual Correction Log'))
butRunMcLog.OnEvent("click", (*) => ControlPaneRuns("butRunMcLog"))
(butFixRep := hh.AddButton('y+5 h25 xm w' . wFactor+370,'Count HotStrings and Potential Fixes'))
butFixRep.OnEvent('Click', StringAndFixReport)


ShowHideButtonsControl(Visibility := False) ; Hides bottom part of GUI as default. 

ControlPaneRuns(buttonIdentifier)
{
	msgbox 'Clicked ' buttonIdentifier
	; if (buttonIdentifier = "butRunAcLog")
	; 	Run VSCodePath "AutoCorrectsLog.ahk" ; <--- butRunAcLog should run this.
	; else if (buttonIdentifier = "butRunMcLog")
	; 	Run VSCodePath "ManualCorrectsLog.ahk" ; <--- butRunMcLog should run this.
}

ShowHideButtonsControl(Visibility := False) ; Shows/Hides bottom, Exam Pane, part of GUI.
{	ControlCmds := [TxtCtrlLbl1,butRunAcLog,butRunMcLog,butFixRep]
	for ctrl in ControlCmds {
		ctrl.Visible := Visibility
	}
}

ShowHideButtonExam(Visibility := False) ; Shows/Hides bottom, Exam Pane, part of GUI.
{	examCmds := [ButLTrim, TxtTypo, ButRTrim, RadBeg, RadMid, RadEnd, ButUndo, TxtTLable, TxtRLable, EdtTMatches, EdtRMatches, TxtWordList]
	for ctrl in examCmds {
		ctrl.Visible := Visibility
	}
}

ExamPaneOpen := 0
ControlPaneOpen := 0

OrigTrigger := "" ; Used to restore original content.
OrigReplacment := ""
tArrStep := [] ; array for trigger undos
rArrStep := [] ; array for replacement undos

;===The=main=function=for=showing=the=Hotstring=Helper=Tool=====================
; This code block copies the selected text, then determines if a hotstring is present.
; If present, hotstring is parsed and HH form is populated and ExamineWords() called. 
; If not, NormalStartup() function is called.
Hotkey hh_Hotkey, CheckClipboard ; Change hotkey above, if desired. 
CheckClipboard(*)
{ 	DefaultHotStr := "" ; Clear each time. 
	TrigLbl.SetFont(FontColor) ; Reset color of Label, in case it's red. 
	EdtRMatches.CurrMatches := "" ; reset custom property
	Global ClipboardOld := ClipboardAll() ; Save and put back later.
	A_Clipboard := ""  ; Must start off blank for detection to work.
	Send("^c") ; Copy selected text.
	Errorlevel := !ClipWait(0.3) ; Wait for clipboard to contain text.

	Global Opts:= "", Trig := "", Repl := "", Opts := ""
	hsRegex := "(?Jim)^:(?<Opts>[^:]+)*:(?<Trig>[^:]+)::(?:f\((?<Repl>[^,)]*)[^)]*\)|(?<Repl>[^;\v]+))?(?<fCom>\h*;\h*(?:\bFIXES\h*\d+\h*WORDS?\b)?(?:\h;)?\h*(?<mCom>.*))?$" ; Jim 156
	; Awesome regex by andymbody: https://www.autohotkey.com/boards/viewtopic.php?f=82&t=125100
	; The regex will detect, and parse, a hotstring, whether normal, or embedded in an f() function. 
	thisHotStr := Trim(A_Clipboard," `t`n`r")
	If RegExMatch(thisHotStr, hsRegex, &hotstr) {
		thisHotStr := "" ; Reset to blank each use.
		TriggerString.text := hotstr.Trig  ; Send to top of GUI. 
		MyDefaultOpts.Value := hotstr.Opts
		sleep(200) ; prevents intermitent error on next line.
		Global OrigTrigger := hotstr.Trig
		hotstr.Repl := Trim(hotstr.Repl, '"')
		ReplaceString.text := hotstr.Repl
		ComStr.text := hotstr.mCom ; Removes autmated part of comment, leaves manual part. 
		Global OrigReplacement := hotstr.Repl
		; ---- For parse text label ----
		Global strT := hotstr.Trig
		Global TrigNeedle_Orig := hotstr.Trig  ; used for TriggerChnged function below.
		Global strR := hotstr.Repl
		hh.origHotStr := hotstr.Repl ; Used if Rarify checkbox undone. 
		; set radio buttons, based on options of copied hotstring... 
		If InStr(hotstr.Opts, "*") && InStr(hotstr.Opts, "?")
			RadMid.Value := 1 ; Set Radio to "middle"
		Else If InStr(hotstr.Opts, "*") 
			RadBeg.Value := 1 ; Set Radio to "beginning"
		Else If InStr(hotstr.Opts, "?")
			RadEnd.Value := 1 ; Set Radio to "end"
		Else
			RadMid.Value := 1 ; Also set Radio to "middle"
		ExamineWords(strT, strR)
	}
	Else {
		Global strT := A_Clipboard
		Global TrigNeedle_Orig := strT ; used for TriggerChnged function below.
		Global strR := A_Clipboard	
		hh.origHotStr := A_Clipboard ; Used if Rarify checkbox undone. 
		NormalStartup(strT, strR)
	}

	Global tMatches := 0 ; <--- Need this or can't run validiy check w/o first filtering. 
	; ---- clear/reset undo history --- 
	ButUndo.Enabled := false
	Loop tArrStep.Length
		tArrStep.pop
	Loop rArrStep.Length
		rArrStep.pop
	; ---------------------------
}

; This function tries to determine if the content of the clipboard is an AutoCorrect
; item, or a selection of boilerplate text.  If boilerplate text, an acronym is
; generated from the first letters.  (e.g. ::ttyl::talk to you later)
NormalStartup(strT, strR)
{	; If multiple spaces or `n present, probably not an Autocorrect entry, so make acronym.
	If ((StrLen(A_Clipboard) - StrLen(StrReplace(A_Clipboard," ")) > 2) || InStr(A_Clipboard, "`n"))
	{	DefaultOpts := DefaultBoilerPlateOpts 
		ReplaceString.value := A_Clipboard
		If (addFirstLetters > 0)
		{ ;LBLhotstring := "Edit trigger string as needed"
			initials := "" ; Initials will be the first letter of each word as a hotstring suggestion.
			HotStrSug := StrReplace(A_Clipboard, "`n", " ") ; Unwrap, but only for hotstr suggestion.
			Loop Parse, HotStrSug, A_Space, A_Tab
			{ 	If (Strlen(A_LoopField) > tooSmallLen) ; Check length of each word, ignore if N letters.
					initials .= SubStr(A_LoopField, "1", "1") 
				If (StrLen(initials) = addFirstLetters) ; stop looping if hotstring is N chars long.
					break
			}
			initials := StrLower(initials)
			; Append preferred prefix or suffix, as defined above, to initials.
			DefaultHotStr := myPrefix . initials . mySuffix
		}
		else 
		{	;LBLhotstring := "Add a trigger string"
			DefaultHotStr := myPrefix . mySuffix ; Use prefix and/or suffix as needed, but no initials.
		}
	}
	Else If (A_Clipboard = "")
	{	;LBLhotstring := "Add a trigger string"
		MyDefaultOpts.Text := "" ; <-- Is this needed?  Might be redundant by Filter() ? 
		TriggerString.Text := "", ReplaceString.Text := "", ComStr.Text := "" ; Clear boxes. 
		RadBeg.Value := 0, RadMid.Value := 0, RadEnd.Value := 0 
		GoFilter()
		hh.Show('Autosize yCenter') 
		Return
	}
	else
	{ ;LBLhotstring := "Add misspelled word"
		; NOTE:  Do we want the copied word to be lower-cased and trimmed of white space?  Methinks, yes. 
		DefaultHotStr := Trim(StrLower(A_Clipboard)) ; No `n found so assume it's a mispelling autocorrect entry: no pre/suffix.
		ReplaceString.value := Trim(StrLower(A_Clipboard)) 
		DefaultOpts := DefaultAutoCorrectOpts  
	}
	
	MyDefaultOpts.text := DefaultOpts
	;TrigLbl.value := LBLhotstring
	TriggerString.value := DefaultHotStr
	ReplaceString.Opt("-Readonly")
	ButApp.Enabled := true
	If ExamPaneOpen = 1
		goFilter()
	hh.Show('Autosize yCenter') 
} 

; The "Exam" button triggers this function.  Most of this function is dedicated
; to comparing/parsing the trigger and replacement to populate the blue Delta String
ExamineWords(strT, strR) 
{	SubTogSize(0, 0) ; Incase size is 'Bigger,' make Smaller.
	hh.Show('Autosize yCenter') 

	ostrT := strT ; original value (not an array)
	ostrR := strR
	LenT := strLen(strT)
	LenR := strLen(strR)

	LoopNum := min(LenT, LenR)
	strT := StrSplit(strT)
	strR := StrSplit(strR)
	Global beginning := ""
	Global typo := ""
	Global fix := ""
	Global ending := ""

	If ostrT = ostrR ; trig/replacement the same
	{	deltaString := "[ " ostrT " | " ostrR " ]"
		found := false ; for duplicate item message, below
	}
	else ; trig/replacement not the same, so find the difference
	{	Loop LoopNum
		{ ; find matching left substring.
			bsubT := (strT[A_Index])
			bsubR := (strR[A_Index])
			If (bsubT = bsubR)
				beginning .= bsubT
			else
				break
		}

		Loop LoopNum
		{ ; Reverse Loop, find matching right substring.
			RevIndex := (LenT - A_Index) + 1
			esubT := (strT[RevIndex])
			RevIndex := (LenR - A_Index) + 1
			esubR := (strR[RevIndex])
			If (esubT = esubR)
				ending := esubT . ending
			else
				break
		}

		If (strLen(beginning) + strLen(ending)) > LoopNum { ; Overlap means repeated chars in trig or replacement.
			If (LenT > LenR) { ; Trig is longer, so use T-R for str len.
				delta := subStr(ending, 1, (LenT - LenR)) ; Left part of ending.  Right part of beginning would also work.
				delta := " [ " . delta . " ||  ] "
			}
			If (LenR > LenT) { ; Replacement is longer, so use R-T for str len.
				delta := subStr(ending, 1, (LenR - LenT))
				delta := " [  ||  " . delta . " ] "
			}
		}
		Else {
			If strLen(beginning) > strLen(ending) { ; replace shorter string last
				typo := StrReplace(ostrT, beginning, "")
				typo := StrReplace(typo, ending, "")
				fix := StrReplace(ostrR, beginning, "")
				fix := StrReplace(fix, ending, "")
			}
			Else {
				typo := StrReplace(ostrT, ending, "")
				typo := StrReplace(typo, beginning, "")
				fix := StrReplace(ostrR, ending, "")
				fix := StrReplace(fix, beginning, "")
			}
			delta := " [ " . typo . " || " . fix . " ] "
		}
		deltaString := beginning . delta . ending

	}		
	; -------------
	TxtTypo.text := deltaString ; set label at top of form.

	ViaExamButt := "Yes"
	GoFilter(ViaExamButt) ; Call filter function then come back here.
	
	If (ButExam.text = "Exam") { 
		ButExam.text := "Done"
		If(hFactor != 0) {
			hh['SizeTog'].text := "Make Bigger"
			SoundBeep
			SubTogSize(0, 0) ; Make replacement edit box small again.
		}
	ShowHideButtonExam(True)	
	}	
	hh.Show('Autosize yCenter') 
}

; This function toggles the size of the HH form, using the above variables.
; HeightSizeIncrease and WidthSizeIncrease determine the size when large.
; The size when small is hardcoded.  Change with caution. 
TogSize(*)
{ 	If (hh['SizeTog'].text = "Make Bigger") { ; Means current state is 'Small'
		hh['SizeTog'].text := "Make Smaller"
		If (ButExam.text = "Done") {
			ShowHideButtonExam(Visibility := False)
			ExamPaneOpen := 0
			ShowHideButtonsControl(Visibility := False)
			ControlPaneOpen := 0
			ButExam.text := "Exam"
		}
		Global hFactor := HeightSizeIncrease
		SubTogSize(hFactor, WidthSizeIncrease)
		;hhButtonExam()
		hh.Show('Autosize yCenter') 
		return
	}
	If (hh['SizeTog'].text = "Make Smaller") { ; Means current state is 'Big'
		hh['SizeTog'].text := "Make Bigger"
		Global hFactor := 0
		SubTogSize(0, 0)
		hh.Show('Autosize yCenter') 
		return
	}
}

; Called by TogSize function. 
SubTogSize(hFactor, wFactor) ; Actually re-draws the form. 
{	;MsgBox("TogSizeFunc`nhFactor is:`n`n" . hFactor)
	TriggerString.Move(, , wFactor + 280,)
	ReplaceString.Move(, , wFactor + 372, hFactor + 100)
	ComLbl.Move(, hFactor + 182, ,)
	ComStr.move(, hFactor + 200, wFactor + 367,)
	ChkFunc.Move(, hFactor + 182, ,)
	ButApp.Move(, hFactor + 234, ,)
	ButCheck.Move(, hFactor + 234, ,)
	ButExam.Move(, hFactor + 234, ,)
	ButSpell.Move(, hFactor + 234, ,)
	ButOpen.Move(, hFactor + 234, ,)
	ButCancel.Move(, hFactor + 234, ,)
}

; This function gets called from hhButtonExam (below) or ButExam's onEvent.
; It shows the Control Pane.
subFuncExamControl(*)
{	Global ControlPaneOpen
	If ControlPaneOpen = 1 {
		ButExam.text := "Exam"
		ShowHideButtonsControl(False)
		ShowHideButtonExam(False)	
		ControlPaneOpen := 0
	}
	Else {
		ButExam.text := "Done"
			;msgbox 'hFactor is ' hFactor 
		If(hFactor = HeightSizeIncrease) { 
			TogSize() ; Make replacement edit box small again.
			hh['SizeTog'].text := "Make Bigger"
		}
		ShowHideButtonsControl(True)
		ShowHideButtonExam(False)	
		ControlPaneOpen := 1
	}
	hh.Show('Autosize yCenter') 
}

hhButtonExam(*) ; Tripple state, but button text is only dual state (exam/done)
{	Global ExamPaneOpen
	Global ControlPaneOpen
	If ((ExamPaneOpen = 0) and (ControlPaneOpen = 0) and GetKeyState("Shift")) 
	|| ((ExamPaneOpen = 1) and (ControlPaneOpen = 0) and GetKeyState("Shift")) { ; Both closed, so open Control Pane.
	subFuncExamControl() ; subFunction shows control pane. 
	}
	Else If (ExamPaneOpen = 0) and (ControlPaneOpen = 0) { ; Both closed, so open Exam Pane.
		ButExam.text := "Done"
		If(hFactor = HeightSizeIncrease) {
			TogSize() ; Make replacement edit box small again.
			hh['SizeTog'].text := "Make Bigger"
		}
		Global OrigTrigger := TriggerString.text
		Global OrigReplacement := ReplaceString.text
		ExamineWords(OrigTrigger, OrigReplacement) 
		goFilter()
		ShowHideButtonsControl(False)
		ShowHideButtonExam(True)	
		ExamPaneOpen := 1
	}
	Else { ; Close either whatever pane is open..
		ButExam.text := "Exam"
		ShowHideButtonsControl(False)
		ShowHideButtonExam(False)
		ExamPaneOpen := 0
		ControlPaneOpen := 0	
	}	
	hh.Show('Autosize yCenter') 	
}

; This functions toggles on/off whether the Pilcrow and other symbols are shown.
; When shown, the replacment box is set "read only" and Append is disabled. 
TogSym(*)
{ 	If (hh['SymTog'].text = "+ Symbols") {
		hh['SymTog'].text := "- Symbols"
		togReplaceString := ReplaceString.text
		togReplaceString := StrReplace(StrReplace(togReplaceString, "`r`n", "`n"), "`n", myPilcrow . "`n") ; Pilcrow for Enter
		togReplaceString := StrReplace(togReplaceString, A_Space, myDot) ; middle dot for Space
		togReplaceString := StrReplace(togReplaceString, A_Tab, myTab) ; space arrow space for Tab
		ReplaceString.value := togReplaceString
		ReplaceString.Opt("+Readonly")
		ButApp.Enabled := false
		hh.Show('Autosize yCenter') 
		return
	}
	If (hh['SymTog'].text = "- Symbols") {
		hh['SymTog'].text := "+ Symbols"
		togReplaceString := ReplaceString.text
		togReplaceString := StrReplace(togReplaceString, myPilcrow . "`r", "`r") ; Have to use `r ... weird.
		togReplaceString := StrReplace(togReplaceString, myDot, A_Space)
		togReplaceString := StrReplace(togReplaceString, myTab, A_Tab)
		ReplaceString.value := togReplaceString
		ReplaceString.Opt("-Readonly")
		ButApp.Enabled := true
		hh.Show('Autosize yCenter') 
		return
	}
}

; The function is called whenever the trigger(hotstring) edit box is changed.  
; It assesses whether a letter has beem manually added to the beginning/ending
; of the trigger, and adds the same letter to the replacement edit box.  
TriggerChanged(*)
{	TrigNeedle_New := TriggerString.text 
	If TrigNeedle_New != TrigNeedle_Orig && ExamPaneOpen = 1 { ; If trigger has changed and pane open.
		If TrigNeedle_Orig = SubStr(TrigNeedle_New, 2, ) { ; one char added on the left left box
			tArrStep.push(TriggerString.text) ; <---- save history for Undo feature
			rArrStep.push(ReplaceString.text) ; <---- save history
			ReplaceString.Value := SubStr(TrigNeedle_New, 1, 1) . ReplaceString.text ; add same char to left of other box
		}
		If TrigNeedle_Orig = SubStr(TrigNeedle_New, 1, StrLen(TrigNeedle_New)-1) { ; one char added on the right or left box
			tArrStep.push(TriggerString.text) ; <---- save history for Undo feature
			rArrStep.push(ReplaceString.text) ; <---- save history
			ReplaceString.text :=  ReplaceString.text . SubStr(TrigNeedle_New, -1, ) ; add same char on other side.
		}
		Global TrigNeedle_Orig := TrigNeedle_New ; Update the "original" string so it can detect the next change.
	}
	ButUndo.Enabled := true
	goFilter()
}

; This function detects that the "[] Make Function" box was ticked. 
; It puts/removes the needed hotstring options, then beeps. 
FormAsFunc(*)
{	If (ChkFunc.Value = 1) {
		MyDefaultOpts.text := "B0X" StrReplace(StrReplace(MyDefaultOpts.text, "B0", ""), "X", "")
		SoundBeep 700, 200
	}
	else {
		MyDefaultOpts.text := StrReplace(StrReplace(MyDefaultOpts.text, "B0", ""), "X", "")
		SoundBeep 900, 200
	}
}

; Runs a validity check.  If validiy problems are found, user is given option to append anyway.  
hhButtonAppend(*)
{ 	Global tMyDefaultOpts := MyDefaultOpts.text
	Global tTriggerString := TriggerString.text
	Global tReplaceString := ReplaceString.text
	ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
	If Not InStr(CombinedValidMsg, "-Okay.", , , 3) ; Msg doesn't have three occurrences of "-Okay." 
		biggerMsgBox(CombinedValidMsg, 1)
	else { ; no validation problems found
		Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
		return
	}
}

; Calls the validity check, but doesn't append the hotstring. 
hhButtonCheck(*)
{ 	Global tMyDefaultOpts := MyDefaultOpts.text
	Global tTriggerString := TriggerString.text
	Global tReplaceString := ReplaceString.text
	ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
	biggerMsgBox(CombinedValidMsg, 0)
	Return
}

; An easy-to-see large dialog to show Validity report/warning. 
biggerMsgBox(thisMess, secondButt)
{	bb := Gui(,'Validity Report')
	bb.SetFont('s11 ' FontColor)
	bb.BackColor := GuiColor, GuiColor
	bb.Add('Text',, 'For proposed new item:').Focus() ; Focusing this prevents the three "edit" boxes from being focus by default.
	bb.SetFont(myBigFont )
	proposedHS := ':' tMyDefaultOpts ':' tTriggerString '::' tReplaceString
	bb.Add('Text', (strLen(proposedHS)>90? 'w600 ':'') 'xs yp+22', proposedHS)
	bb.SetFont('s11 ')
	secondButt=0? bb.Add('Text', ,"===Validation Check Results==="):''
	
	bb.SetFont(myBigFont )
	bbItem := StrSplit(thisMess, "*|*") 
	; Use "edit" rather than "text" because it allows us to select the text. 
	edtSharedSettings := ' -VScroll ReadOnly -E0x200 Background'
	bb.Add('Edit', (inStr(bbItem[1],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[1]) 
	bb.Add('Edit', (strLen(bbItem[2])>104? ' w600 ' : ' ') (inStr(bbItem[2],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[2]) 
	bb.Add('Edit', (strLen(bbItem[3])>104? ' w600 ' : ' ') (inStr(bbItem[3],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[3]) 

	bb.SetFont('s11 ' FontColor)
	secondButt=1? bb.Add('Text',,"==============================`nAppend HotString Anyway?"):''
	bbAppend := bb.Add('Button', , 'Append Anyway')
	bbAppend.OnEvent 'Click', (*) => Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
	bbAppend.OnEvent 'Click', (*) => bb.Destroy()
	if secondButt != 1
		bbAppend.Visible := False
	bbClose := bb.Add('Button', 'x+5 Default', 'Close')
	bbClose.OnEvent 'Click', (*) => bb.Destroy()
	bb.Show('yCenter x' (A_ScreenWidth/2))
	WinSetAlwaysontop(1, "A")
	bb.OnEvent 'Escape', (*) => bb.Destroy()
}

; This function runs several validity checks. 
ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
{ 	GoFilter() ; This ensures that "rMatches" has been populated. <--- had it commented out for a while, then put back. 
	Global CombinedValidMsg := "", validHotDupes := "", validHotMisspells := ""
	ThisFile := Fileread(A_ScriptName) ; Save these contents to variable 'ThisFile'.
	If (tMyDefaultOpts = "") ; If options box is empty, skip regxex check.
		validOpts := "Okay."
	else { ;===== Make sure hotstring options are valid ========
		NeedleRegEx := "(\*|B0|\?|SI|C|K[0-9]{1,3}|SE|X|SP|O|R|T)" ; These are in the AHK docs I swear!!!
		WithNeedlesRemoved := RegExReplace(tMyDefaultOpts, NeedleRegEx, "") ; Remove all valid options from var.
		If (WithNeedlesRemoved = "") ; If they were all removed...
			validOpts := "Okay."
		else { ; Some characters from the Options box were not recognized.
			OptTips := inStr(WithNeedlesRemoved, ":")? "Don't include the colons.`n":""
			OptTips .= " ;  a block text assignement to var
			(
			...Tips from AHK v1 docs...
			* - ending char not needed
			? - trigger inside other words
			B0 - no backspacing
			SI - send input mode
			C - case-sensitive
			K(n) - set key delay
			SE - send event mode
			X - execute command
			SP - send play mode
			O - omit end char
			R - send raw
			T - super raw
			)"
			validOpts .= "Invalid Hotsring Options found.`n---> " WithNeedlesRemoved "`n" OptTips
		}
	}
	;==== Make sure hotstring box content is valid ========
	validHot := "" ; Reset to empty each time.
	If (tTriggerString = "") || (tTriggerString = myPrefix) || (tTriggerString = mySuffix) 
		validHot := "HotString box should not be empty."
	Else If InStr(tTriggerString, ":")
		validHot := "Don't include colons."
	else ; No colons, and not empty. Good. Now check for duplicates.
	{	getStartLineNumber() ; No need to check hh2 code for duplicates... 
		Loop Parse, ThisFile, "`n", "`r" { ; Check line-by-line.
			If (A_Index < ACitemsStartAt) or (SubStr(trim(A_LoopField, " `t"), 1,1) != ":") 
				continue ; Will skip non-hotstring lines, so the regex isn't used as much.
			If RegExMatch(A_LoopField, "i):(?P<Opts>[^:]+)*:(?P<Trig>[^:]+)", &loo) { ; loo is "current loopfield"
				If (tTriggerString = loo.Trig) and (tMyDefaultOpts = loo.Opts) { ; full duplicate triggers
					validHotDupes := "Duplicate trigger string found at line " A_Index ".`n---> " A_LoopField
					break
				} ; No duplicates.  Look for conflicts... 
				Else If (InStr(loo.Trig, tTriggerString) and inStr(tMyDefaultOpts, "*") and inStr(tMyDefaultOpts, "?"))
				|| (InStr(tTriggerString, loo.Trig) and inStr(loo.Opts, "*") and  inStr(loo.Opts, "?")) { ; Word-Middle Matches
					validHotDupes := "Word-Middle conflict found at line " A_Index ", where one of the strings will be nullified by the other.`n---> " A_LoopField 
					break
				}
				Else If ((loo.Trig = tTriggerString) and inStr(loo.Opts, "*") and inStr(tMyDefaultOpts, "?"))
				|| ((tTriggerString = loo.Trig) and inStr(loo.Opts, "?") and inStr(tMyDefaultOpts, "*")) { ; Rule out: Same word, but beginning and end opts
					validHotDupes := "Duplicate trigger found at line " A_Index ", but maybe okay, because one is word-beginning and other is word-ending.`n---> " A_LoopField 
					Break
				}
				If (inStr(loo.Opts, "*") and loo.Trig = subStr(tTriggerString, 1, strLen(loo.Trig)))
				|| (inStr(tMyDefaultOpts, "*") and tTriggerString = subStr(loo.Trig, 1, strLen(tTriggerString))) { ; Word-Beginning Matches
					validHotDupes := "Word Beginning conflict found at line " A_Index ", where one of the strings is a subset of the other.  Whichever appears last will never be expanded.`n---> " A_LoopField
					break
				}
				Else If (inStr(loo.Opts, "?") and loo.Trig = subStr(tTriggerString, -strLen(loo.Trig)))
				|| (inStr(tMyDefaultOpts, "?") and tTriggerString = subStr(loo.Trig, -strLen(tTriggerString))) { ; Word-Ending Matches
					validHotDupes := "Word Ending conflict found at line " A_Index ", where one of the strings is a superset of the other.  The longer of the strings should appear before the other, in your code.`n---> " A_LoopField
					break
				}
			}
			Else ; not a regex match, so go to next loop.
				continue 
		}	
		If (tMatches > 0){ ; This error message is collected separately from the loop, so both can potentially be reported. 
			validHotMisspells := "This trigger string will misspell [" tMatches "] words."
		}
		if validHotDupes and validHotMisspells
			validHot := validHotDupes "`n-" validHotMisspells ; neither is blank, so new line
		else If !validHotDupes  and !validHotMisspells ; both are blank, so no validity concerns. 
			validHot := "Okay."
		else 
		validHot := validHotDupes  validHotMisspells ; one (and only one) is blank so concantinate
	}

	;==== Make sure replacement string box content is valid ===========
	If (tReplaceString = "")
		validRep := "Replacement string box should not be empty."
	else if (SubStr(tReplaceString, 1, 1) == ":") ; If Replacement box empty, or first char is ":"
		validRep := "Don't include the colons."
	else if  (tReplaceString = tTriggerString)
		validRep := "Replacement string SAME AS Trigger string."
	else
		validRep := "Okay."
	; Concatenate the three above validity checks.
	CombinedValidMsg := "OPTIONS BOX `n-" . validOpts . "*|*HOTSTRING BOX `n-" . validHot . "*|*REPLACEMENT BOX `n-" . validRep
	Return CombinedValidMsg ; return result for use is Append or Validation functions.
} ; end of validation func

; The "Append It" function actually combines the hotsring components and 
; appends them to the script, then reloads it. 
Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
{ 	WholeStr := ""
	tMyDefaultOpts := MyDefaultOpts.text
	tTriggerString := TriggerString.text
	tReplaceString := ReplaceString.text
	; tComStr := hh['ComStr'].text ; tComStr is "text of comment string." <--- not used?
	tComStr := '' ; tComStr is "text of comment string."
	aComStr := '' ; aComStr is "auto comment string." Default to blank each time. 
	
	If (rMatches > 0) and (AutoCommentFixesAndMisspells = 1) ; AutoCom var set near top of code. 
	{ 	Misspells := ""
		Misspells := EdtTMatches.Value
		If (tMatches > 3) ; and (Misspells != "") ; More than 3 misspellings?
			Misspells := ", but misspells " . tMatches . " words !!! "
		Else If (Misspells != "") { ; any misspellings? List them, if <= 3.
		; Misspells := StrReplace(Misspells, "`n", " (), ")
			Misspells := SubStr(StrReplace(Misspells, "`n", " (), "), 1, -2) . ". "
			Misspells := ", but misspells " . Misspells
			;MsgBox("tMatches " . tMatches . "`nMisspells is:`n`n" . Misspells)
		}
		aComStr := "Fixes " . rMatches . " words " . Misspells
		aComStr := StrReplace(aComStr, "Fixes 1 words ", "Fixes 1 word ")
	}

	fopen := '' , fclose := ''
	If (chkFunc.Value = 1) ; add function part if needed
		{
			tMyDefaultOpts := "B0X" . StrReplace(tMyDefaultOpts, "B0X", "")
			fopen := 'f("'
			fclose := '")'
		}
	
	If (ComStr.text != "") || (aComStr != "")
		tComStr := " `; " . aComStr . ComStr.text

	If InStr(tReplaceString, "`n") { ; Combine the parts into a muli-line hotstring.
		openParenth := subStr(tReplaceString, -1) = "`t"? "(RTrim0`n" : "(`n" ; If last char is Tab, use LTrim0.
		WholeStr := ":" . tMyDefaultOpts . ":" . tTriggerString . "::" . tComStr . "`n" . fopen . openParenth . tReplaceString . "`n)" . fclose
	}
	Else ; Combine the parts into a single-line hotstring.
		WholeStr := ":" . tMyDefaultOpts . ":" . tTriggerString . "::" . fopen . tReplaceString . fclose . tComStr
			
	If GetKeyState("Shift") { ; User held Shift when clicking Append Button. 
		A_Clipboard := WholeStr
		SoundBeep 800, 200
		SoundBeep 700, 300
		; MsgBox "in appendit(), clipbrd is`n" A_Clipboard
	}
	else
	{ 	FileAppend("`n" WholeStr, A_ScriptFullPath) ; 'n makes sure it goes on a new line.
		If not getKeyState("Ctrl")
			Reload() ; relaod the script so the new hotstring will be ready for use; but not if ctrl pressed.
	}
}  ; Newly added hotstrings will be way at the bottom of the ahk file.

; Calls the Google "Did you mean..." function below. 
hhButtonSpell(*) ; Called it "Spell" because "Spell Check" is too long.
{ tReplaceString := ReplaceString.text
	If (tReplaceString = "")
		MsgBox("Replacement Text not found.", , 4096)
	else {
		googleSugg := GoogleAutoCorrect(tReplaceString) ; Calls below function
		If (googleSugg = "")
			MsgBox("No suggestions found.", , 4096)
		Else {
			msgResult := MsgBox(googleSugg "`n`n######################`nChange Replacement Text?", "Google Suggestion", "OC 4096")
			if (msgResult = "OK") {
				ReplaceString.value := googleSugg
				goFilter()
			}
			else
				return
		}
	}
}

GoogleAutoCorrect(word)
{ 	; Original by TheDewd, converted to v2 by Mikeyww.
	; autohotkey.com/boards/viewtopic.php?f=82&t=120143
	objReq := ComObject('WinHttp.WinHttpRequest.5.1')
	objReq.Open('GET', 'https://www.google.com/search?q=' word)
	objReq.SetRequestHeader('User-Agent'
		, 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)')
	objReq.Send(), HTML := objReq.ResponseText
	If RegExMatch(HTML, 'value="(.*?)"', &A)
		If RegExMatch(HTML, ';spell=1.*?>(.*?)<\/a>', &B)
			Return B[1] || A[1]
}

; Opens this file and go to the bottom so you can see your Hotstrings.
hhButtonOpen(*)
{  	hh.Hide()
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
	Edit()
	WinWaitActive(A_ScriptName) ; Wait for the script to be open in text editor.
	Sleep(250)
	Send("{Ctrl Down}{End}{Ctrl Up}{Home}") ; Navigate to the bottom.
}

; Double-clicking name of Word List file (bottom of Exam Pane) calls this function.
; Close/hide from and resport clipboard contents. 
; Open this file and browse to locaton of Word List assignment, then open folder. 
ChangeWordList(*)
{	hh.Hide()
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
	Edit()
	WinWaitActive(A_ScriptName) ; Wait for the script to be open in text editor.
	Sleep(250)
	SendInput "^f"
	Sleep(100)
	SendInput WordListFile ; Enters file name into search box.
	Sleep(250)
	Run strReplace(WordListPath, "\" . WordListFile, "") ; Opens word list folder in file browser. 
}

; Close/hide form, clear everything, and restore clipboard contents. 
hhButtonCancel(*)
{ 	hh.Hide()
	MyDefaultOpts.value := ""
	TriggerString.value := ""
	ReplaceString.value := ""
	tArrStep := [] ; array for trigger undos
	rArrStep := [] ; array for replacement undos
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
}

GoLTrim(*) ; Trim one char from left of trigger and replacement.
{		;---- trig -----
	tText := TriggerString.value
	tArrStep.push(tText) ; <---- save history for Undo feature
	tText := subStr(tText, 2)
	TriggerString.value := tText
		; ----- repl -----
	rText := ReplaceString.value
	rArrStep.push(rText) ; <---- save history
	rText := subStr(rText, 2)
	ReplaceString.value := rText
	; -----------
	ButUndo.Enabled := true
	TriggerChanged() 
}

GoRTrim(*) ; Trim one char from right of trigger and replacement.
{		; ----- trig -----
	tText := TriggerString.value
	tArrStep.push(tText) ; <---- save history
	tText := subStr(tText, 1, strLen(tText) - 1)
	TriggerString.value := tText
		; ----- repl -----
	rText := ReplaceString.value
	rArrStep.push(rText) ; <---- save history
	rText := subStr(rText, 1, strLen(rText) - 1)
	ReplaceString.value := rText
	; -----------
	ButUndo.Enabled := true
	TriggerChanged() 
}

; Left and Right Trims are saved in Arrays.  This function removes the last one. 
GoUndo(*)
{ 	If GetKeyState("Shift") 
		GoReStart() 
	else If (tArrStep.Length > 0) and (rArrStep.Length > 0) {
		TriggerString.value := tArrStep.Pop()
		ReplaceString.value := rArrStep.Pop()
		GoFilter()
	}
	else {
		ButUndo.Enabled := false
	}
}
; ReEnters the trigger and replacement that were gotten from RegEx upon first capture.
; Clears arrays.  Has effect of "undoing" all of the changes. 
GoReStart(*)
{ 	If !OrigTrigger and !OrigReplacment
		MsgBox("Can't restart -- Nothing in memory...")
	Else {
		TriggerString.Value := OrigTrigger ; Restore original values. 
		ReplaceString.Value := OrigReplacement
		ButUndo.Enabled := false
		tArrStep := [] ; Reset arrays to nothing.
		rArrStep := []
		GoFilter()
	}
}

; Single-click of middle radio button just calls GoFilter function but 
; double-click sets button to false.  
clickLast := 0
GoMidRadio(*)
{	global clickCurrent := A_TickCount
	if (clickCurrent - clickLast < 500) { ; Simulates watching for double-click.
		RadMid.Value := 0 ; Set middle radio to blank, and removed hotstring options. 
		MyDefaultOpts.text := strReplace(strReplace(MyDefaultOpts.text, "?", ""), "*", "")
	}
	global clickLast := A_TickCount
	GoFilter()
}

; Filters the two lists of words at bottom of Exam Pane. 
; Mostly this is called via L/R trims, or by changing radio buttons.  
; If it is called via Exam button, then reads Options box and updates radios.
GoFilter(ViaExamButt := "No", *) ; Filter the big list of words, as needed.
{	; ====== Hotstring/Trigger ===========
	tFind := Trim(TriggerString.Value)
	If !tFind
		tFind := " " ; prevents error if tFind is blank.
	tFilt := ''
	Global tMatches := 0 ; Global so I can read it in the Validation() function.
	MyOpts := MyDefaultOpts.text 

	If (ViaExamButt = "Yes") { ; Read opts box, change radios as needed.
		If  inStr(MyOpts, "*") and  inStr(MyOpts, "?")
			RadMid.value := 1
		Else if  inStr(MyOpts, "*")
			RadBeg.value := 1
		Else if  inStr(MyOpts, "?")
			RadEnd.value := 1
		Else {
			RadMid.value := 0
			RadBeg.value := 0
			RadEnd.value := 0
		}
	}

	Loop Read, WordListPath ; Compare with the big list of words and find matches.
	{
		If InStr(A_LoopReadLine, tFind) {
			IF (RadMid.value = 1) {
				tFilt .= A_LoopReadLine '`n'
				tMatches++
			}
			Else If (RadEnd.value = 1) {
				If InStr(SubStr(A_LoopReadLine, -StrLen(tFind)), tFind) {
					tFilt .= A_LoopReadLine '`n'
					tMatches++
				}
			}
			else If (RadBeg.value = 1) {
				If InStr(SubStr(A_LoopReadLine, 1, StrLen(tFind)), tFind) {
					tFilt .= A_LoopReadLine '`n'
					tMatches++
				}
			}
			Else {
				If (A_LoopReadLine = tFind) {
					tFilt := tFind
					tMatches++
				}
			}
		}
	}

	IF (RadMid.value = 1) {
		If not inStr(MyOpts, "*")
			MyOpts := MyOpts . "*"
		If not inStr(MyOpts, "?")
			MyOpts := MyOpts . "?"
	}
	Else If (RadEnd.value = 1) {
		If not inStr(MyOpts, "?")
			MyOpts := MyOpts . "?"
			MyOpts := StrReplace(MyOpts, "*")
	}
	else If (RadBeg.value = 1) {
		If not inStr(MyOpts, "*")
			MyOpts := MyOpts . "*"
			MyOpts := StrReplace(MyOpts, "?")
	}
	MyDefaultOpts.text := MyOpts

	EdtTMatches.Value := tFilt
	TxtTLable.Text := "Misspells [" . tMatches . "]"
	
	If (tMatches > 0) { 
		TrigLbl.Text := "Misspells [" . tMatches . "] words" ; Change Trig Str Label to show warning. 
		TrigLbl.SetFont("cRed")
	}
	If (tMatches = 0) {
		TrigLbl.Text := "No Misspellings found." ; Change Trig Str Label to NO LONGER show warning. 
		TrigLbl.SetFont(FontColor) ; reset color of Label, incase it's red. 
	}

	; ====== Replacement/Expansion text ==========
	rFind := Trim(ReplaceString.Value, "`n`t ")
		If !rFind
		rFind := " " ; prevents error if rFind is blank.
	rFilt := ''
	Global rMatches := 0
	
	Loop Read WordListPath  ; Compare with the big list of words and find matches.
	{
		If InStr(A_LoopReadLine, rFind) {
			IF (RadMid.value = 1) { 
				rFilt .= A_LoopReadLine '`n'

				rMatches++
			}
			Else If (RadEnd.value = 1) {
				If InStr(SubStr(A_LoopReadLine, -StrLen(rFind)), rFind) {
					rFilt .= A_LoopReadLine '`n'
					rMatches++
				}
			}
			else If (RadBeg.value = 1) { ; 'Beg' radio.
				If InStr(SubStr(A_LoopReadLine, 1, StrLen(rFind)), rFind) {
					rFilt .= A_LoopReadLine '`n'
					rMatches++
				}
			}
			Else {
				If (A_LoopReadLine = rFind) {
					rFilt := rFind
					rMatches++
				}
			}
		}
	}
	EdtRMatches.Value := rFilt
	TxtRLable.Text := "Fixes [" . rMatches . "]"
}


; ################ END of HH2 ###########################################################
; ...............................QQQ.....................QQQQQQ.....QQQ.........QQQ......
; ...............................QQQ.....................QQQQQ......QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ..QQQQQQ....QQQQQQQQQ....QQQQQQQQQ..........QQQQQQ..QQQQQQQQ......QQQQQQQQQ...QQQQQQQQQ
; .QQQQQQQQ...QQQQQQQQQQ..QQQQQQQQQQ.........QQQQQQQQ.QQQQQQQQ......QQQQQQQQQQ..QQQQQQQQQ
; QQQQ.QQQQQ..QQQQQ.QQQQ..QQQQ.QQQQQ........QQQQ.QQQQQ..QQQQ........QQQQQ.QQQQ..QQQQQ.QQQ
; QQQ....QQQ..QQQQ...QQQ.QQQQ...QQQQ.......QQQQ....QQQ..QQQQ........QQQQ...QQQ..QQQQ...QQ
; QQQ....QQQ..QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ....QQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQQQQQQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQQQQQQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQ..........QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ.........QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ....QQQ..QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQ..QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQ..QQQQ..QQQ....QQQ..QQQQ.QQQQQ........QQQQ.QQQQQ..QQQQ........QQQ....QQQ..QQQ....QQ
; .QQQQQQQQ...QQQ....QQQ...QQQQQQQQQ.........QQQQQQQQ...QQQQ........QQQ....QQQ..QQQ....QQ
; ..QQQQQQ....QQQ....QQQ....QQQQQQQQ..........QQQQQQ....QQQQ........QQQ....QQQ..QQQ....QQ
; #######################################################################################



;###############################################
; Number of "potential fixes" based on WordWeb app, and varies greatly by word list used. 
^F3:: ; Ctrl+F3: Report information about the autocorrect items.
StringAndFixReport(*)
{	ThisFile := FileRead(A_ScriptFullPath)
	thisOptions := '', regulars := 0, begins := 0, middles := 0, ends := 0, fixes := 0, entries := 0
	Loop Parse ThisFile, '`n'
	{	If SubStr(Trim(A_LoopField),1,1) != ':'
			continue
		entries++
		thisOptions := SubStr(Trim(A_LoopField), 1, InStr(A_LoopField, ':',,,2)) ; get options part of hotstring
		If InStr(thisOptions, '*') and InStr(thisOptions, '?')
			middles++
		Else If InStr(thisOptions, '*')
			begins++
		Else If InStr(thisOptions, '?')
			ends++
		Else
			regulars++
		If RegExMatch(A_LoopField, 'Fixes\h*\K\d+', &fn) ; Need a regex for this... 
			fixes += fn[]
	}
	MsgBox( '   Totals`n===========================`n    Regular Autocorrects:`t   ' numberFormat(regulars)
	'`n    Word Beginnings:`t`t' numberFormat(begins)
	'`n    Word Middles:`t`t' numberFormat(middles)
	'`n    Word Ends:`t`t   ' numberFormat(ends) ; this is a smaller number, so push over with '  ', simulates right justification.
	'`n===========================`n   Total Entries:`t`t' numberFormat(entries)
	'`n   Potential Fixes:`t`t' numberFormat(fixes) 
	, 'Report for ' A_ScriptName, 64
	)
	numberFormat(num) ; Function to format a number with commas (by ChatGPT4)
	{	global
		Loop 
		{	oldnum := num
			num := RegExReplace(num, "(\d)(\d{3}(\,|$))", "$1,$2") ; search for number patterns and insert commas
			if (num == oldnum) ; If the number doesn't change, exit the loop
				break
		}
		return num
	}
}
;###############################################


#Hotstring Z ; The Z causes the end char to be reset after each activation. 

;============== Determine start line of autocorrect items ======================
; Trigger String duplicate items validity check will skip lines of code before ACitemsStartAt var value.
; Gets called from Validity Check function. 
getStartLineNumber(*)
{	For idx, line in StrSplit(FileRead(A_ScriptName), "`n")
		If inStr(line, "AUTOCORRECT START POINT MARKER") { ; <--- Sees itself..  LOL 
			Global ACitemsStartAt := idx + 8 ; Should equal line number where autocorrect list starts. 
			Break ; We found it, so no need to keep looping.  
		}
	Return ACitemsStartAt
}
;===============================================================================

EDIT: How about this (below) for the logic on the "periodic log intervals"? I thought about using a temp file also, but, presumably, holding everything in RAM requires fewer hard drive read/writes. (?)

Code: Select all

#SingleInstance
#Requires AutoHotkey v2+
; timed append testing ... also append on exit ... turn off timer if no new text for 2 timer cycles. 

mygui := Gui()
thisText := mygui.addEdit('w200','')
mygui.addButton('w200','Keep Text').OnEvent('click', keepText)
mygui.Show()

logIsRunning := 0
savedUpText := ''
intervalCounter := 0  ; Initialize the counter

; There's no point running the logger if no text has been saved up...  
; So don't run timer when script starts.  Run it when logging starts. 
keepText(*)
{   If thisText.Text = ''
        return
    global savedUpText .= thisText.Text '`n'
    thisText.Text := ''
    global intervalCounter := 0  	; Reset the counter since we're adding new text
    If logIsRunning = 0  			; only start the timer it it is not already running.
        setTimer Appender, 5000  	; call function every 5 secs.
}

; Gets called by timer, or by onExit.
Appender(*) 
{   FileAppend savedUpText, "timedAppendTextLog.txt"
    global savedUpText := ''  		; clear each time, since text has been logged.
	global logIsRunning := 1  		; set to 1 so we don't keep resetting the timer.
    global intervalCounter += 1 	; Increments here, but resets in other locations. 
    If (intervalCounter > 1)  		; Check if no text has been kept for 2 intervals
    {   setTimer Appender, 0  		; Turn off the timer
        global logIsRunning := 0  	; Indicate that the timer is no longer running
        global intervalCounter := 0 ; Reset the counter for safety
    }
    SoundBeep 1200, 400
    SoundBeep 1200, 400
}

OnExit Appender 					; Also append one more time on exit. 

esc::ExitApp

setTimer debug, 20 ; just for debugging. 
debug(*)
{ 
	tooltip (
	'logIsRunning ' 		logIsRunning		
	'`nintervalCounter '	intervalCounter
	'`n`nsavedUpText' 		savedUpText
	), 100, 100
}

ste(phen|ve) kunkel

Jasonosaj
Posts: 51
Joined: 02 Feb 2022, 15:02
Location: California

Re: AutoCorrect for v2

Post by Jasonosaj » 29 Feb 2024, 19:22

kunkel321 wrote:
29 Feb 2024, 09:29
@Jasonosaj This is awesome! I still need to study it more, but I definitely like how you are relying more on function-call parameters, and less on global variables. I cringe every time I add another global variable, because there are so many, and I know it's not best practice to over-use them. That said... I've already added more since posting the last zip-- LOL.

@Descolada I think I will see about adding support for _HS() formatted hotstrings. It's worth noting that the HotString Helper 2.0 (hh2) code and the f() function code are totally separate (though in the same ahk file). So having a "_HS() version of hh2" doesn't involve changing _HS nor f() in any way. Adding support should be easy. The code to add "_HS" rather than "f()" is a single line of code. The only thing really that will need to be changed is that _HS uses the second parameter for holding certain hotstring options. That will need to be accommodated.

Also... Good point about the problem of constant logging. Thanks for pointing that out. For a couple of years, I've been using a laptop with dual solid state drives, so it's not an issue, but if a person is using a spinning hard disc, then the constant logging must get annoying. f() doesn't really support case conformation by the way. Though the f() and the _HS() functions would both (sort of) support an initial capital if/when the entire trigger was not backspaced.

EDIT: fyi @Jasonosaj here is the latest version of hh2. I had already expanded on the validation function a bit. And the validation "msgbox" is now a big (easy to see) colorful gui window. The Exam button is now multi-functional. Right-click (or shift+left click) on it for the "control pane" which has some links to related tool.

fyi I anyone tries the below code, they'll need the subfolder and wordlist that are in the zip, attached on page two of this thread... Here viewtopic.php?f=83&t=120220&start=20#p559328
EDIT again 2:30pm PST:
- r-click on Exam button now toggles more smartly.
- validity messages in "big MsgBox" are now selectable. ;)

Code: Select all

#SingleInstance
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode("RegEx")
#Requires AutoHotkey v2+

;===============================================================================
;            			Hotstring Helper 2.0
;          Hotkey: Win + H | By: Kunkel321 | Version: 2-28-2024
; https://www.autohotkey.com/boards/viewtopic.php?f=6&t=114688
; A version of Hotstring Helper that will support block multi-line replacements and 
; allow user to examine hotstring for multi-word matches. The "Examine/Analyze" 
; pop-down part of the form is based on the WAG tool here
; https://www.autohotkey.com/boards/viewtopic.php?f=83&t=120377
; Customization options are below, near top of code.
; Please get a copy of AutoHotkey.exe (v2) and rename it to match the name of this
; script file, so that the .exe and the .ahk have the same name, in the same folder.
; DO NOT COMPILE, or the Append command won't work. The Gui stays in RAM, but gets
; repopulated upon hotkey press. HotStrings will be appended (added) by the
; script at the bottom. Shift+Append saves to clipboard instead of appending. 
; This tool is intended to be embedded in your AutoCorrect list.
;===============================================================================

;==Change=color=of=Hotstring=Helper=form=as=desired===========================
GuiColor := "F5F5DC" ; "F0F8FF" is light blue. Tip: Use "Default" for Windows default.
FontColor := "003366" ; "003366" is dark blue. Tip: Use "Default" for Windows default.

; ===Change=Settings=for=Big=Validity=Dialog=Message=Box========================
myGreen := 'c1D7C08' ; light green 'cB5FFA4' (for use with dark backgrounds.)
myRed := 'cB90012' ; light red 'cFFB2AD'
myBigFont := 's13'

;==Change=Hotstring=Helper=Activation=Hotkey=as=desired=========================
hh_Hotkey := "#h" ; The activation hotkey-combo (not string) is Win+h. 

;==Change=title=of=Hotstring=Helper=form=as=desired=============================
hhFormName := "HotString Helper 2.0" ; The name at the top of the form. Change here, if desired.

; ======Change=size=of=GUI=when="Make Bigger"=is=invoked========================
HeightSizeIncrease := 300 ; Numbers, not 'strings,' so no quotation marks. 
WidthSizeIncrease := 400

;====Assign=symbols=for="Show Symb"=button======================================
myPilcrow := "¶"    ; Okay to change symbols if desired.
myDot := "• "       ; adding a space (optional) allows more natural wrapping.
myTab := "⟹ "      ; adding a space (optional) allows more natural wrapping.

;===Change=options=for=MULTI=word=entry=options=and=trigger=strings=as=desired==
; These are the defaults for "acronym" based boiler plate template trigger strings. 
DefaultBoilerPlateOpts := ""  ; PreEnter these multi-word hotstring options; "*" = end char not needed, etc.
myPrefix := ";"        ; Optional character that you want suggested at the beginning of each hotstring.
addFirstLetters := 5   ; Add first letter of this many words. (5 recommended; 0 = don't use feature.)
tooSmallLen := 2       ; Only first letters from words longer than this. (Moot if addFirstLetters = 0)
mySuffix := ""         ; An empty string "" means don't use feature.

;===============Change=options=AUTOCORRECT=words=as=desired=====================
; PreEnter these (single-word) autocorrect options; "T" = raw text mode, etc.
DefaultAutoCorrectOpts := "*" ; An empty string "" means don't use feature.

;=====List=of=words=use=for=examination=lookup==================================
WordListFile := 'GitHubComboList249k.txt' ; Mostly from github: Copyright (c) 2020 Wordnik
; WordListFile := 'wlist_match6.txt' ; From https://www.keithv.com/software/wlist/

;=====Other=Settings============================================================
; Add "Fixes X words, but misspells Y" to the end of autocorrect items. 
; 1 = Yes, 0 = No. Multi-line Continuation Section items are never auto-commented.
AutoCommentFixesAndMisspells := 1

;====Window=specific=hotkeys====================================================
; These can be edited... Cautiously. 
#HotIf WinActive(hhFormName) ; Allows window-specific hotkeys.
$Enter:: ; When Enter is pressed, but only in this GUI. "$" prevents accidental Enter key loop.
{ 	If (hh['SymTog'].text = "Hide Symb")
		return ; If 'Show symbols' is active, do nothing.
	Else if ReplaceString.Focused {
		Send("{Enter}") ; Just normal typing; Enter yields Enter key press.
		Return
	}
	Else hhButtonAppend() ; Replacement box not focused, so press Append button.
}
+Left:: ; Shift+Left: Got to trigger, move cursor far left.
{	TriggerString.Focus()
		Send "{Home}"
}
Esc::
{ 	hh.Hide()
	A_Clipboard := ClipboardOld
}
^z:: GoUndo() ; Undo last 'word exam' trims, one at a time.
^+z:: GoReStart() ; Put the whole trigger and replacement back (restart).
^Up:: 		; Ctrl+Up Arrow, or 
^WheelUp::	; Ctrl+Mouse Wheel Up to increase font size (toggle, not zoom.)
{	MyDefaultOpts.SetFont('s15')  ; sets at 15
	TriggerString.SetFont('s15')
	ReplaceString.SetFont('s15')
}
^Down:: 		; Ctrl+Down Arrow, or 
^WheelDown:: 	; Ctrl+Mouse Wheel Down to put font size back.
{	MyDefaultOpts.SetFont('s11')  ; sets back at 11
	TriggerString.SetFont('s11')
	ReplaceString.SetFont('s11')
}
#HotIf ; Turn off window-specific behavior.


; Make sure word list is there. Change name of word list subfolder, if desired. 
WordListPath := A_ScriptDir '\WordListsForHH\' WordListFile
If not FileExist(WordListPath)
	MsgBox("This error means that the big list of comparison words at:`n" . WordListPath . 
	"`nwas not found.`n`nTherefore the 'Exam' button of the Hotstring Helper tool won't work.")
SplitPath WordListPath, &WordListName ; Extract just the name of the file.

;===== Main Graphical User Interface (GUI) is built here =======================
hh := Gui('', hhFormName)
hh.Opt("-MinimizeBox +alwaysOnTop")
hh.BackColor := GuiColor
FontColor := FontColor != "" ? "c" . FontColor : ""
hh.SetFont("s11 " . FontColor)
hFactor := 0, wFactor := 0 ; Don't change size here. 
; -----  Trigger string parts ----
hh.AddText('y4 w30', 'Options')
(TrigLbl := hh.AddText('x+40 w250', 'Trigger String'))
(MyDefaultOpts := hh.AddEdit('yp+20 xm+2 w70 h24'))
(TriggerString := hh.AddEdit('x+18 w' . wFactor + 280, '')).OnEvent('Change', TriggerChanged)
; ----- Replacement string parts ----
hh.AddText('xm', 'Replacement')
hh.SetFont('s9')
hh.AddButton('vSizeTog x+75 yp-5 h8 +notab', 'Make Bigger').OnEvent("Click", TogSize)
hh.AddButton('vSymTog x+5 h8 +notab', '+ Symbols').OnEvent("Click", TogSym)
hh.SetFont('s11')
(ReplaceString := hh.AddEdit('vReplaceString +Wrap y+1 xs h' . hFactor + 100 . ' w' . wFactor + 370, '')).OnEvent('Change', GoFilter)
; ---- Below Replacement ----
ComLbl := hh.AddText('xm y' . hFactor + 182, 'Comment')
(ChkFunc := hh.AddCheckbox('vFunc, x+70 y' . hFactor + 182, 'Make Function')).onEvent('click', FormAsFunc)
ChkFunc.Value := 1 ; 'Make Function' box checked by default?  1 = checked.  
hh.SetFont("s11 cGreen")
ComStr := hh.AddEdit('vComStr xs y' . hFactor + 200 . ' w' . wFactor + 370)
hh.SetFont("s11 " . FontColor)
; ---- Buttons ----
(ButApp := hh.AddButton('xm y' . hFactor + 234, 'Append')).OnEvent("Click", hhButtonAppend)
(ButCheck := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Check')).OnEvent("Click", hhButtonCheck)
(ButExam := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Exam'))
ButExam.OnEvent("Click", hhButtonExam)
ButExam.OnEvent("ContextMenu", subFuncExamControl)
(ButSpell := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Spell')).OnEvent("Click", hhButtonSpell)
(ButOpen := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Open')).OnEvent("Click", hhButtonOpen)
(ButCancel := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Cancel')).OnEvent("Click", hhButtonCancel)
hh.OnEvent("Close", hhButtonCancel)
; ============== Bottom (toggling) "Exam Pane" part of GUI =====================
; ---- delta string ----
hh.SetFont('s10')
(ButLTrim := hh.AddButton('vbutLtrim xm h50  w' . (wFactor+182/6), '>>')).onEvent('click', GoLTrim)
hh.SetFont('s14')
(TxtTypo := hh.AddText('vTypoLabel -wrap +center cBlue x+1 w' . (wFactor+182*5/3), hhFormName))
hh.SetFont('s10')
(ButRTrim := hh.AddButton('vbutRtrim x+1 h50 w' . (wFactor+182/6), '<<')).onEvent('click', GoRTrim)
; ---- radio buttons -----
hh.SetFont('s11')
(RadBeg := hh.AddRadio('vBegRadio y+-18 x' . (wFactor+182/3), '&Beginnings')).onEvent('click', GoFilter)
(RadMid := hh.AddRadio('vMidRadio x+5', '&Middles')).onEvent('click', GoMidRadio)
(RadEnd := hh.AddRadio('vEndRadio x+5', '&Endings')).onEvent('click', GoFilter)
; ---- bottom buttons -----
(ButUndo := hh.AddButton('xm y+3 h26 w' . (wFactor+182*2), "Undo (+Reset)")).OnEvent('Click', GoUndo)
ButUndo.Enabled := false
; ---- results lists -----
hh.SetFont('s12')
(TxtTLable := hh.AddText('vTrigLabel center y+4 h25 xm w' . wFactor+182, 'Misspells'))
(TxtRLable := hh.AddText('vReplLabel center h25 x+5 w' . wFactor+182, 'Fixes'))
(EdtTMatches := hh.AddEdit('vTrigMatches y+1 xm h' . hFactor+300 . ' w' . wFactor+182,))
(EdtRMatches := hh.AddEdit('vReplMatches x+5 h' . hFactor+300 . ' w' . wFactor+182,))
; ---- word list file ----
hh.SetFont('bold s10')
(TxtWordList := hh.AddText('vWordList center xm y+1 h14 w' . wFactor*2+364 , WordListName)).OnEvent('DoubleClick', ChangeWordList)
ShowHideButtonExam(Visibility := False) ; Hides bottom part of GUI as default. 
; ============== Bottom (toggling) "Control Pane" part of GUI =====================
(TxtCtrlLbl1 := hh.AddText(' center cBlue ym+270 h25 xm w' . wFactor+370, 'Secret Control Panel!'))
hh.SetFont('s10')
(butRunAcLog := hh.AddButton('  y+5 h25 xm w' . wFactor+370, 'Open AutoCorrection Log'))
butRunAcLog.OnEvent("click", (*) => ControlPaneRuns("butRunAcLog"))
(butRunMcLog := hh.AddButton('  y+5 h25 xm w' . wFactor+370, 'Open Manual Correction Log'))
butRunMcLog.OnEvent("click", (*) => ControlPaneRuns("butRunMcLog"))
(butFixRep := hh.AddButton('y+5 h25 xm w' . wFactor+370,'Count HotStrings and Potential Fixes'))
butFixRep.OnEvent('Click', StringAndFixReport)


ShowHideButtonsControl(Visibility := False) ; Hides bottom part of GUI as default. 

ControlPaneRuns(buttonIdentifier)
{
	msgbox 'Clicked ' buttonIdentifier
	; if (buttonIdentifier = "butRunAcLog")
	; 	Run VSCodePath "AutoCorrectsLog.ahk" ; <--- butRunAcLog should run this.
	; else if (buttonIdentifier = "butRunMcLog")
	; 	Run VSCodePath "ManualCorrectsLog.ahk" ; <--- butRunMcLog should run this.
}

ShowHideButtonsControl(Visibility := False) ; Shows/Hides bottom, Exam Pane, part of GUI.
{	ControlCmds := [TxtCtrlLbl1,butRunAcLog,butRunMcLog,butFixRep]
	for ctrl in ControlCmds {
		ctrl.Visible := Visibility
	}
}

ShowHideButtonExam(Visibility := False) ; Shows/Hides bottom, Exam Pane, part of GUI.
{	examCmds := [ButLTrim, TxtTypo, ButRTrim, RadBeg, RadMid, RadEnd, ButUndo, TxtTLable, TxtRLable, EdtTMatches, EdtRMatches, TxtWordList]
	for ctrl in examCmds {
		ctrl.Visible := Visibility
	}
}

ExamPaneOpen := 0
ControlPaneOpen := 0

OrigTrigger := "" ; Used to restore original content.
OrigReplacment := ""
tArrStep := [] ; array for trigger undos
rArrStep := [] ; array for replacement undos

;===The=main=function=for=showing=the=Hotstring=Helper=Tool=====================
; This code block copies the selected text, then determines if a hotstring is present.
; If present, hotstring is parsed and HH form is populated and ExamineWords() called. 
; If not, NormalStartup() function is called.
Hotkey hh_Hotkey, CheckClipboard ; Change hotkey above, if desired. 
CheckClipboard(*)
{ 	DefaultHotStr := "" ; Clear each time. 
	TrigLbl.SetFont(FontColor) ; Reset color of Label, in case it's red. 
	EdtRMatches.CurrMatches := "" ; reset custom property
	Global ClipboardOld := ClipboardAll() ; Save and put back later.
	A_Clipboard := ""  ; Must start off blank for detection to work.
	Send("^c") ; Copy selected text.
	Errorlevel := !ClipWait(0.3) ; Wait for clipboard to contain text.

	Global Opts:= "", Trig := "", Repl := "", Opts := ""
	hsRegex := "(?Jim)^:(?<Opts>[^:]+)*:(?<Trig>[^:]+)::(?:f\((?<Repl>[^,)]*)[^)]*\)|(?<Repl>[^;\v]+))?(?<fCom>\h*;\h*(?:\bFIXES\h*\d+\h*WORDS?\b)?(?:\h;)?\h*(?<mCom>.*))?$" ; Jim 156
	; Awesome regex by andymbody: https://www.autohotkey.com/boards/viewtopic.php?f=82&t=125100
	; The regex will detect, and parse, a hotstring, whether normal, or embedded in an f() function. 
	thisHotStr := Trim(A_Clipboard," `t`n`r")
	If RegExMatch(thisHotStr, hsRegex, &hotstr) {
		thisHotStr := "" ; Reset to blank each use.
		TriggerString.text := hotstr.Trig  ; Send to top of GUI. 
		MyDefaultOpts.Value := hotstr.Opts
		sleep(200) ; prevents intermitent error on next line.
		Global OrigTrigger := hotstr.Trig
		hotstr.Repl := Trim(hotstr.Repl, '"')
		ReplaceString.text := hotstr.Repl
		ComStr.text := hotstr.mCom ; Removes autmated part of comment, leaves manual part. 
		Global OrigReplacement := hotstr.Repl
		; ---- For parse text label ----
		Global strT := hotstr.Trig
		Global TrigNeedle_Orig := hotstr.Trig  ; used for TriggerChnged function below.
		Global strR := hotstr.Repl
		hh.origHotStr := hotstr.Repl ; Used if Rarify checkbox undone. 
		; set radio buttons, based on options of copied hotstring... 
		If InStr(hotstr.Opts, "*") && InStr(hotstr.Opts, "?")
			RadMid.Value := 1 ; Set Radio to "middle"
		Else If InStr(hotstr.Opts, "*") 
			RadBeg.Value := 1 ; Set Radio to "beginning"
		Else If InStr(hotstr.Opts, "?")
			RadEnd.Value := 1 ; Set Radio to "end"
		Else
			RadMid.Value := 1 ; Also set Radio to "middle"
		ExamineWords(strT, strR)
	}
	Else {
		Global strT := A_Clipboard
		Global TrigNeedle_Orig := strT ; used for TriggerChnged function below.
		Global strR := A_Clipboard	
		hh.origHotStr := A_Clipboard ; Used if Rarify checkbox undone. 
		NormalStartup(strT, strR)
	}

	Global tMatches := 0 ; <--- Need this or can't run validiy check w/o first filtering. 
	; ---- clear/reset undo history --- 
	ButUndo.Enabled := false
	Loop tArrStep.Length
		tArrStep.pop
	Loop rArrStep.Length
		rArrStep.pop
	; ---------------------------
}

; This function tries to determine if the content of the clipboard is an AutoCorrect
; item, or a selection of boilerplate text.  If boilerplate text, an acronym is
; generated from the first letters.  (e.g. ::ttyl::talk to you later)
NormalStartup(strT, strR)
{	; If multiple spaces or `n present, probably not an Autocorrect entry, so make acronym.
	If ((StrLen(A_Clipboard) - StrLen(StrReplace(A_Clipboard," ")) > 2) || InStr(A_Clipboard, "`n"))
	{	DefaultOpts := DefaultBoilerPlateOpts 
		ReplaceString.value := A_Clipboard
		If (addFirstLetters > 0)
		{ ;LBLhotstring := "Edit trigger string as needed"
			initials := "" ; Initials will be the first letter of each word as a hotstring suggestion.
			HotStrSug := StrReplace(A_Clipboard, "`n", " ") ; Unwrap, but only for hotstr suggestion.
			Loop Parse, HotStrSug, A_Space, A_Tab
			{ 	If (Strlen(A_LoopField) > tooSmallLen) ; Check length of each word, ignore if N letters.
					initials .= SubStr(A_LoopField, "1", "1") 
				If (StrLen(initials) = addFirstLetters) ; stop looping if hotstring is N chars long.
					break
			}
			initials := StrLower(initials)
			; Append preferred prefix or suffix, as defined above, to initials.
			DefaultHotStr := myPrefix . initials . mySuffix
		}
		else 
		{	;LBLhotstring := "Add a trigger string"
			DefaultHotStr := myPrefix . mySuffix ; Use prefix and/or suffix as needed, but no initials.
		}
	}
	Else If (A_Clipboard = "")
	{	;LBLhotstring := "Add a trigger string"
		MyDefaultOpts.Text := "" ; <-- Is this needed?  Might be redundant by Filter() ? 
		TriggerString.Text := "", ReplaceString.Text := "", ComStr.Text := "" ; Clear boxes. 
		RadBeg.Value := 0, RadMid.Value := 0, RadEnd.Value := 0 
		GoFilter()
		hh.Show('Autosize yCenter') 
		Return
	}
	else
	{ ;LBLhotstring := "Add misspelled word"
		; NOTE:  Do we want the copied word to be lower-cased and trimmed of white space?  Methinks, yes. 
		DefaultHotStr := Trim(StrLower(A_Clipboard)) ; No `n found so assume it's a mispelling autocorrect entry: no pre/suffix.
		ReplaceString.value := Trim(StrLower(A_Clipboard)) 
		DefaultOpts := DefaultAutoCorrectOpts  
	}
	
	MyDefaultOpts.text := DefaultOpts
	;TrigLbl.value := LBLhotstring
	TriggerString.value := DefaultHotStr
	ReplaceString.Opt("-Readonly")
	ButApp.Enabled := true
	If ExamPaneOpen = 1
		goFilter()
	hh.Show('Autosize yCenter') 
} 

; The "Exam" button triggers this function.  Most of this function is dedicated
; to comparing/parsing the trigger and replacement to populate the blue Delta String
ExamineWords(strT, strR) 
{	SubTogSize(0, 0) ; Incase size is 'Bigger,' make Smaller.
	hh.Show('Autosize yCenter') 

	ostrT := strT ; original value (not an array)
	ostrR := strR
	LenT := strLen(strT)
	LenR := strLen(strR)

	LoopNum := min(LenT, LenR)
	strT := StrSplit(strT)
	strR := StrSplit(strR)
	Global beginning := ""
	Global typo := ""
	Global fix := ""
	Global ending := ""

	If ostrT = ostrR ; trig/replacement the same
	{	deltaString := "[ " ostrT " | " ostrR " ]"
		found := false ; for duplicate item message, below
	}
	else ; trig/replacement not the same, so find the difference
	{	Loop LoopNum
		{ ; find matching left substring.
			bsubT := (strT[A_Index])
			bsubR := (strR[A_Index])
			If (bsubT = bsubR)
				beginning .= bsubT
			else
				break
		}

		Loop LoopNum
		{ ; Reverse Loop, find matching right substring.
			RevIndex := (LenT - A_Index) + 1
			esubT := (strT[RevIndex])
			RevIndex := (LenR - A_Index) + 1
			esubR := (strR[RevIndex])
			If (esubT = esubR)
				ending := esubT . ending
			else
				break
		}

		If (strLen(beginning) + strLen(ending)) > LoopNum { ; Overlap means repeated chars in trig or replacement.
			If (LenT > LenR) { ; Trig is longer, so use T-R for str len.
				delta := subStr(ending, 1, (LenT - LenR)) ; Left part of ending.  Right part of beginning would also work.
				delta := " [ " . delta . " ||  ] "
			}
			If (LenR > LenT) { ; Replacement is longer, so use R-T for str len.
				delta := subStr(ending, 1, (LenR - LenT))
				delta := " [  ||  " . delta . " ] "
			}
		}
		Else {
			If strLen(beginning) > strLen(ending) { ; replace shorter string last
				typo := StrReplace(ostrT, beginning, "")
				typo := StrReplace(typo, ending, "")
				fix := StrReplace(ostrR, beginning, "")
				fix := StrReplace(fix, ending, "")
			}
			Else {
				typo := StrReplace(ostrT, ending, "")
				typo := StrReplace(typo, beginning, "")
				fix := StrReplace(ostrR, ending, "")
				fix := StrReplace(fix, beginning, "")
			}
			delta := " [ " . typo . " || " . fix . " ] "
		}
		deltaString := beginning . delta . ending

	}		
	; -------------
	TxtTypo.text := deltaString ; set label at top of form.

	ViaExamButt := "Yes"
	GoFilter(ViaExamButt) ; Call filter function then come back here.
	
	If (ButExam.text = "Exam") { 
		ButExam.text := "Done"
		If(hFactor != 0) {
			hh['SizeTog'].text := "Make Bigger"
			SoundBeep
			SubTogSize(0, 0) ; Make replacement edit box small again.
		}
	ShowHideButtonExam(True)	
	}	
	hh.Show('Autosize yCenter') 
}

; This function toggles the size of the HH form, using the above variables.
; HeightSizeIncrease and WidthSizeIncrease determine the size when large.
; The size when small is hardcoded.  Change with caution. 
TogSize(*)
{ 	If (hh['SizeTog'].text = "Make Bigger") { ; Means current state is 'Small'
		hh['SizeTog'].text := "Make Smaller"
		If (ButExam.text = "Done") {
			ShowHideButtonExam(Visibility := False)
			ExamPaneOpen := 0
			ShowHideButtonsControl(Visibility := False)
			ControlPaneOpen := 0
			ButExam.text := "Exam"
		}
		Global hFactor := HeightSizeIncrease
		SubTogSize(hFactor, WidthSizeIncrease)
		;hhButtonExam()
		hh.Show('Autosize yCenter') 
		return
	}
	If (hh['SizeTog'].text = "Make Smaller") { ; Means current state is 'Big'
		hh['SizeTog'].text := "Make Bigger"
		Global hFactor := 0
		SubTogSize(0, 0)
		hh.Show('Autosize yCenter') 
		return
	}
}

; Called by TogSize function. 
SubTogSize(hFactor, wFactor) ; Actually re-draws the form. 
{	;MsgBox("TogSizeFunc`nhFactor is:`n`n" . hFactor)
	TriggerString.Move(, , wFactor + 280,)
	ReplaceString.Move(, , wFactor + 372, hFactor + 100)
	ComLbl.Move(, hFactor + 182, ,)
	ComStr.move(, hFactor + 200, wFactor + 367,)
	ChkFunc.Move(, hFactor + 182, ,)
	ButApp.Move(, hFactor + 234, ,)
	ButCheck.Move(, hFactor + 234, ,)
	ButExam.Move(, hFactor + 234, ,)
	ButSpell.Move(, hFactor + 234, ,)
	ButOpen.Move(, hFactor + 234, ,)
	ButCancel.Move(, hFactor + 234, ,)
}

; This function gets called from hhButtonExam (below) or ButExam's onEvent.
; It shows the Control Pane.
subFuncExamControl(*)
{	Global ControlPaneOpen
	If ControlPaneOpen = 1 {
		ButExam.text := "Exam"
		ShowHideButtonsControl(False)
		ShowHideButtonExam(False)	
		ControlPaneOpen := 0
	}
	Else {
		ButExam.text := "Done"
			;msgbox 'hFactor is ' hFactor 
		If(hFactor = HeightSizeIncrease) { 
			TogSize() ; Make replacement edit box small again.
			hh['SizeTog'].text := "Make Bigger"
		}
		ShowHideButtonsControl(True)
		ShowHideButtonExam(False)	
		ControlPaneOpen := 1
	}
	hh.Show('Autosize yCenter') 
}

hhButtonExam(*) ; Tripple state, but button text is only dual state (exam/done)
{	Global ExamPaneOpen
	Global ControlPaneOpen
	If ((ExamPaneOpen = 0) and (ControlPaneOpen = 0) and GetKeyState("Shift")) 
	|| ((ExamPaneOpen = 1) and (ControlPaneOpen = 0) and GetKeyState("Shift")) { ; Both closed, so open Control Pane.
	subFuncExamControl() ; subFunction shows control pane. 
	}
	Else If (ExamPaneOpen = 0) and (ControlPaneOpen = 0) { ; Both closed, so open Exam Pane.
		ButExam.text := "Done"
		If(hFactor = HeightSizeIncrease) {
			TogSize() ; Make replacement edit box small again.
			hh['SizeTog'].text := "Make Bigger"
		}
		Global OrigTrigger := TriggerString.text
		Global OrigReplacement := ReplaceString.text
		ExamineWords(OrigTrigger, OrigReplacement) 
		goFilter()
		ShowHideButtonsControl(False)
		ShowHideButtonExam(True)	
		ExamPaneOpen := 1
	}
	Else { ; Close either whatever pane is open..
		ButExam.text := "Exam"
		ShowHideButtonsControl(False)
		ShowHideButtonExam(False)
		ExamPaneOpen := 0
		ControlPaneOpen := 0	
	}	
	hh.Show('Autosize yCenter') 	
}

; This functions toggles on/off whether the Pilcrow and other symbols are shown.
; When shown, the replacment box is set "read only" and Append is disabled. 
TogSym(*)
{ 	If (hh['SymTog'].text = "+ Symbols") {
		hh['SymTog'].text := "- Symbols"
		togReplaceString := ReplaceString.text
		togReplaceString := StrReplace(StrReplace(togReplaceString, "`r`n", "`n"), "`n", myPilcrow . "`n") ; Pilcrow for Enter
		togReplaceString := StrReplace(togReplaceString, A_Space, myDot) ; middle dot for Space
		togReplaceString := StrReplace(togReplaceString, A_Tab, myTab) ; space arrow space for Tab
		ReplaceString.value := togReplaceString
		ReplaceString.Opt("+Readonly")
		ButApp.Enabled := false
		hh.Show('Autosize yCenter') 
		return
	}
	If (hh['SymTog'].text = "- Symbols") {
		hh['SymTog'].text := "+ Symbols"
		togReplaceString := ReplaceString.text
		togReplaceString := StrReplace(togReplaceString, myPilcrow . "`r", "`r") ; Have to use `r ... weird.
		togReplaceString := StrReplace(togReplaceString, myDot, A_Space)
		togReplaceString := StrReplace(togReplaceString, myTab, A_Tab)
		ReplaceString.value := togReplaceString
		ReplaceString.Opt("-Readonly")
		ButApp.Enabled := true
		hh.Show('Autosize yCenter') 
		return
	}
}

; The function is called whenever the trigger(hotstring) edit box is changed.  
; It assesses whether a letter has beem manually added to the beginning/ending
; of the trigger, and adds the same letter to the replacement edit box.  
TriggerChanged(*)
{	TrigNeedle_New := TriggerString.text 
	If TrigNeedle_New != TrigNeedle_Orig && ExamPaneOpen = 1 { ; If trigger has changed and pane open.
		If TrigNeedle_Orig = SubStr(TrigNeedle_New, 2, ) { ; one char added on the left left box
			tArrStep.push(TriggerString.text) ; <---- save history for Undo feature
			rArrStep.push(ReplaceString.text) ; <---- save history
			ReplaceString.Value := SubStr(TrigNeedle_New, 1, 1) . ReplaceString.text ; add same char to left of other box
		}
		If TrigNeedle_Orig = SubStr(TrigNeedle_New, 1, StrLen(TrigNeedle_New)-1) { ; one char added on the right or left box
			tArrStep.push(TriggerString.text) ; <---- save history for Undo feature
			rArrStep.push(ReplaceString.text) ; <---- save history
			ReplaceString.text :=  ReplaceString.text . SubStr(TrigNeedle_New, -1, ) ; add same char on other side.
		}
		Global TrigNeedle_Orig := TrigNeedle_New ; Update the "original" string so it can detect the next change.
	}
	ButUndo.Enabled := true
	goFilter()
}

; This function detects that the "[] Make Function" box was ticked. 
; It puts/removes the needed hotstring options, then beeps. 
FormAsFunc(*)
{	If (ChkFunc.Value = 1) {
		MyDefaultOpts.text := "B0X" StrReplace(StrReplace(MyDefaultOpts.text, "B0", ""), "X", "")
		SoundBeep 700, 200
	}
	else {
		MyDefaultOpts.text := StrReplace(StrReplace(MyDefaultOpts.text, "B0", ""), "X", "")
		SoundBeep 900, 200
	}
}

; Runs a validity check.  If validiy problems are found, user is given option to append anyway.  
hhButtonAppend(*)
{ 	Global tMyDefaultOpts := MyDefaultOpts.text
	Global tTriggerString := TriggerString.text
	Global tReplaceString := ReplaceString.text
	ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
	If Not InStr(CombinedValidMsg, "-Okay.", , , 3) ; Msg doesn't have three occurrences of "-Okay." 
		biggerMsgBox(CombinedValidMsg, 1)
	else { ; no validation problems found
		Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
		return
	}
}

; Calls the validity check, but doesn't append the hotstring. 
hhButtonCheck(*)
{ 	Global tMyDefaultOpts := MyDefaultOpts.text
	Global tTriggerString := TriggerString.text
	Global tReplaceString := ReplaceString.text
	ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
	biggerMsgBox(CombinedValidMsg, 0)
	Return
}

; An easy-to-see large dialog to show Validity report/warning. 
biggerMsgBox(thisMess, secondButt)
{	bb := Gui(,'Validity Report')
	bb.SetFont('s11 ' FontColor)
	bb.BackColor := GuiColor, GuiColor
	bb.Add('Text',, 'For proposed new item:').Focus() ; Focusing this prevents the three "edit" boxes from being focus by default.
	bb.SetFont(myBigFont )
	proposedHS := ':' tMyDefaultOpts ':' tTriggerString '::' tReplaceString
	bb.Add('Text', (strLen(proposedHS)>90? 'w600 ':'') 'xs yp+22', proposedHS)
	bb.SetFont('s11 ')
	secondButt=0? bb.Add('Text', ,"===Validation Check Results==="):''
	
	bb.SetFont(myBigFont )
	bbItem := StrSplit(thisMess, "*|*") 
	; Use "edit" rather than "text" because it allows us to select the text. 
	edtSharedSettings := ' -VScroll ReadOnly -E0x200 Background'
	bb.Add('Edit', (inStr(bbItem[1],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[1]) 
	bb.Add('Edit', (strLen(bbItem[2])>104? ' w600 ' : ' ') (inStr(bbItem[2],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[2]) 
	bb.Add('Edit', (strLen(bbItem[3])>104? ' w600 ' : ' ') (inStr(bbItem[3],'-Okay.')? myGreen : myRed) edtSharedSettings GuiColor, bbItem[3]) 

	bb.SetFont('s11 ' FontColor)
	secondButt=1? bb.Add('Text',,"==============================`nAppend HotString Anyway?"):''
	bbAppend := bb.Add('Button', , 'Append Anyway')
	bbAppend.OnEvent 'Click', (*) => Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
	bbAppend.OnEvent 'Click', (*) => bb.Destroy()
	if secondButt != 1
		bbAppend.Visible := False
	bbClose := bb.Add('Button', 'x+5 Default', 'Close')
	bbClose.OnEvent 'Click', (*) => bb.Destroy()
	bb.Show('yCenter x' (A_ScreenWidth/2))
	WinSetAlwaysontop(1, "A")
	bb.OnEvent 'Escape', (*) => bb.Destroy()
}

; This function runs several validity checks. 
ValidationFunction(tMyDefaultOpts, tTriggerString, tReplaceString)
{ 	GoFilter() ; This ensures that "rMatches" has been populated. <--- had it commented out for a while, then put back. 
	Global CombinedValidMsg := "", validHotDupes := "", validHotMisspells := ""
	ThisFile := Fileread(A_ScriptName) ; Save these contents to variable 'ThisFile'.
	If (tMyDefaultOpts = "") ; If options box is empty, skip regxex check.
		validOpts := "Okay."
	else { ;===== Make sure hotstring options are valid ========
		NeedleRegEx := "(\*|B0|\?|SI|C|K[0-9]{1,3}|SE|X|SP|O|R|T)" ; These are in the AHK docs I swear!!!
		WithNeedlesRemoved := RegExReplace(tMyDefaultOpts, NeedleRegEx, "") ; Remove all valid options from var.
		If (WithNeedlesRemoved = "") ; If they were all removed...
			validOpts := "Okay."
		else { ; Some characters from the Options box were not recognized.
			OptTips := inStr(WithNeedlesRemoved, ":")? "Don't include the colons.`n":""
			OptTips .= " ;  a block text assignement to var
			(
			...Tips from AHK v1 docs...
			* - ending char not needed
			? - trigger inside other words
			B0 - no backspacing
			SI - send input mode
			C - case-sensitive
			K(n) - set key delay
			SE - send event mode
			X - execute command
			SP - send play mode
			O - omit end char
			R - send raw
			T - super raw
			)"
			validOpts .= "Invalid Hotsring Options found.`n---> " WithNeedlesRemoved "`n" OptTips
		}
	}
	;==== Make sure hotstring box content is valid ========
	validHot := "" ; Reset to empty each time.
	If (tTriggerString = "") || (tTriggerString = myPrefix) || (tTriggerString = mySuffix) 
		validHot := "HotString box should not be empty."
	Else If InStr(tTriggerString, ":")
		validHot := "Don't include colons."
	else ; No colons, and not empty. Good. Now check for duplicates.
	{	getStartLineNumber() ; No need to check hh2 code for duplicates... 
		Loop Parse, ThisFile, "`n", "`r" { ; Check line-by-line.
			If (A_Index < ACitemsStartAt) or (SubStr(trim(A_LoopField, " `t"), 1,1) != ":") 
				continue ; Will skip non-hotstring lines, so the regex isn't used as much.
			If RegExMatch(A_LoopField, "i):(?P<Opts>[^:]+)*:(?P<Trig>[^:]+)", &loo) { ; loo is "current loopfield"
				If (tTriggerString = loo.Trig) and (tMyDefaultOpts = loo.Opts) { ; full duplicate triggers
					validHotDupes := "Duplicate trigger string found at line " A_Index ".`n---> " A_LoopField
					break
				} ; No duplicates.  Look for conflicts... 
				Else If (InStr(loo.Trig, tTriggerString) and inStr(tMyDefaultOpts, "*") and inStr(tMyDefaultOpts, "?"))
				|| (InStr(tTriggerString, loo.Trig) and inStr(loo.Opts, "*") and  inStr(loo.Opts, "?")) { ; Word-Middle Matches
					validHotDupes := "Word-Middle conflict found at line " A_Index ", where one of the strings will be nullified by the other.`n---> " A_LoopField 
					break
				}
				Else If ((loo.Trig = tTriggerString) and inStr(loo.Opts, "*") and inStr(tMyDefaultOpts, "?"))
				|| ((tTriggerString = loo.Trig) and inStr(loo.Opts, "?") and inStr(tMyDefaultOpts, "*")) { ; Rule out: Same word, but beginning and end opts
					validHotDupes := "Duplicate trigger found at line " A_Index ", but maybe okay, because one is word-beginning and other is word-ending.`n---> " A_LoopField 
					Break
				}
				If (inStr(loo.Opts, "*") and loo.Trig = subStr(tTriggerString, 1, strLen(loo.Trig)))
				|| (inStr(tMyDefaultOpts, "*") and tTriggerString = subStr(loo.Trig, 1, strLen(tTriggerString))) { ; Word-Beginning Matches
					validHotDupes := "Word Beginning conflict found at line " A_Index ", where one of the strings is a subset of the other.  Whichever appears last will never be expanded.`n---> " A_LoopField
					break
				}
				Else If (inStr(loo.Opts, "?") and loo.Trig = subStr(tTriggerString, -strLen(loo.Trig)))
				|| (inStr(tMyDefaultOpts, "?") and tTriggerString = subStr(loo.Trig, -strLen(tTriggerString))) { ; Word-Ending Matches
					validHotDupes := "Word Ending conflict found at line " A_Index ", where one of the strings is a superset of the other.  The longer of the strings should appear before the other, in your code.`n---> " A_LoopField
					break
				}
			}
			Else ; not a regex match, so go to next loop.
				continue 
		}	
		If (tMatches > 0){ ; This error message is collected separately from the loop, so both can potentially be reported. 
			validHotMisspells := "This trigger string will misspell [" tMatches "] words."
		}
		if validHotDupes and validHotMisspells
			validHot := validHotDupes "`n-" validHotMisspells ; neither is blank, so new line
		else If !validHotDupes  and !validHotMisspells ; both are blank, so no validity concerns. 
			validHot := "Okay."
		else 
		validHot := validHotDupes  validHotMisspells ; one (and only one) is blank so concantinate
	}

	;==== Make sure replacement string box content is valid ===========
	If (tReplaceString = "")
		validRep := "Replacement string box should not be empty."
	else if (SubStr(tReplaceString, 1, 1) == ":") ; If Replacement box empty, or first char is ":"
		validRep := "Don't include the colons."
	else if  (tReplaceString = tTriggerString)
		validRep := "Replacement string SAME AS Trigger string."
	else
		validRep := "Okay."
	; Concatenate the three above validity checks.
	CombinedValidMsg := "OPTIONS BOX `n-" . validOpts . "*|*HOTSTRING BOX `n-" . validHot . "*|*REPLACEMENT BOX `n-" . validRep
	Return CombinedValidMsg ; return result for use is Append or Validation functions.
} ; end of validation func

; The "Append It" function actually combines the hotsring components and 
; appends them to the script, then reloads it. 
Appendit(tMyDefaultOpts, tTriggerString, tReplaceString)
{ 	WholeStr := ""
	tMyDefaultOpts := MyDefaultOpts.text
	tTriggerString := TriggerString.text
	tReplaceString := ReplaceString.text
	; tComStr := hh['ComStr'].text ; tComStr is "text of comment string." <--- not used?
	tComStr := '' ; tComStr is "text of comment string."
	aComStr := '' ; aComStr is "auto comment string." Default to blank each time. 
	
	If (rMatches > 0) and (AutoCommentFixesAndMisspells = 1) ; AutoCom var set near top of code. 
	{ 	Misspells := ""
		Misspells := EdtTMatches.Value
		If (tMatches > 3) ; and (Misspells != "") ; More than 3 misspellings?
			Misspells := ", but misspells " . tMatches . " words !!! "
		Else If (Misspells != "") { ; any misspellings? List them, if <= 3.
		; Misspells := StrReplace(Misspells, "`n", " (), ")
			Misspells := SubStr(StrReplace(Misspells, "`n", " (), "), 1, -2) . ". "
			Misspells := ", but misspells " . Misspells
			;MsgBox("tMatches " . tMatches . "`nMisspells is:`n`n" . Misspells)
		}
		aComStr := "Fixes " . rMatches . " words " . Misspells
		aComStr := StrReplace(aComStr, "Fixes 1 words ", "Fixes 1 word ")
	}

	fopen := '' , fclose := ''
	If (chkFunc.Value = 1) ; add function part if needed
		{
			tMyDefaultOpts := "B0X" . StrReplace(tMyDefaultOpts, "B0X", "")
			fopen := 'f("'
			fclose := '")'
		}
	
	If (ComStr.text != "") || (aComStr != "")
		tComStr := " `; " . aComStr . ComStr.text

	If InStr(tReplaceString, "`n") { ; Combine the parts into a muli-line hotstring.
		openParenth := subStr(tReplaceString, -1) = "`t"? "(RTrim0`n" : "(`n" ; If last char is Tab, use LTrim0.
		WholeStr := ":" . tMyDefaultOpts . ":" . tTriggerString . "::" . tComStr . "`n" . fopen . openParenth . tReplaceString . "`n)" . fclose
	}
	Else ; Combine the parts into a single-line hotstring.
		WholeStr := ":" . tMyDefaultOpts . ":" . tTriggerString . "::" . fopen . tReplaceString . fclose . tComStr
			
	If GetKeyState("Shift") { ; User held Shift when clicking Append Button. 
		A_Clipboard := WholeStr
		SoundBeep 800, 200
		SoundBeep 700, 300
		; MsgBox "in appendit(), clipbrd is`n" A_Clipboard
	}
	else
	{ 	FileAppend("`n" WholeStr, A_ScriptFullPath) ; 'n makes sure it goes on a new line.
		If not getKeyState("Ctrl")
			Reload() ; relaod the script so the new hotstring will be ready for use; but not if ctrl pressed.
	}
}  ; Newly added hotstrings will be way at the bottom of the ahk file.

; Calls the Google "Did you mean..." function below. 
hhButtonSpell(*) ; Called it "Spell" because "Spell Check" is too long.
{ tReplaceString := ReplaceString.text
	If (tReplaceString = "")
		MsgBox("Replacement Text not found.", , 4096)
	else {
		googleSugg := GoogleAutoCorrect(tReplaceString) ; Calls below function
		If (googleSugg = "")
			MsgBox("No suggestions found.", , 4096)
		Else {
			msgResult := MsgBox(googleSugg "`n`n######################`nChange Replacement Text?", "Google Suggestion", "OC 4096")
			if (msgResult = "OK") {
				ReplaceString.value := googleSugg
				goFilter()
			}
			else
				return
		}
	}
}

GoogleAutoCorrect(word)
{ 	; Original by TheDewd, converted to v2 by Mikeyww.
	; autohotkey.com/boards/viewtopic.php?f=82&t=120143
	objReq := ComObject('WinHttp.WinHttpRequest.5.1')
	objReq.Open('GET', 'https://www.google.com/search?q=' word)
	objReq.SetRequestHeader('User-Agent'
		, 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)')
	objReq.Send(), HTML := objReq.ResponseText
	If RegExMatch(HTML, 'value="(.*?)"', &A)
		If RegExMatch(HTML, ';spell=1.*?>(.*?)<\/a>', &B)
			Return B[1] || A[1]
}

; Opens this file and go to the bottom so you can see your Hotstrings.
hhButtonOpen(*)
{  	hh.Hide()
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
	Edit()
	WinWaitActive(A_ScriptName) ; Wait for the script to be open in text editor.
	Sleep(250)
	Send("{Ctrl Down}{End}{Ctrl Up}{Home}") ; Navigate to the bottom.
}

; Double-clicking name of Word List file (bottom of Exam Pane) calls this function.
; Close/hide from and resport clipboard contents. 
; Open this file and browse to locaton of Word List assignment, then open folder. 
ChangeWordList(*)
{	hh.Hide()
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
	Edit()
	WinWaitActive(A_ScriptName) ; Wait for the script to be open in text editor.
	Sleep(250)
	SendInput "^f"
	Sleep(100)
	SendInput WordListFile ; Enters file name into search box.
	Sleep(250)
	Run strReplace(WordListPath, "\" . WordListFile, "") ; Opens word list folder in file browser. 
}

; Close/hide form, clear everything, and restore clipboard contents. 
hhButtonCancel(*)
{ 	hh.Hide()
	MyDefaultOpts.value := ""
	TriggerString.value := ""
	ReplaceString.value := ""
	tArrStep := [] ; array for trigger undos
	rArrStep := [] ; array for replacement undos
	A_Clipboard := ClipboardOld  ; Restore previous contents of clipboard.
}

GoLTrim(*) ; Trim one char from left of trigger and replacement.
{		;---- trig -----
	tText := TriggerString.value
	tArrStep.push(tText) ; <---- save history for Undo feature
	tText := subStr(tText, 2)
	TriggerString.value := tText
		; ----- repl -----
	rText := ReplaceString.value
	rArrStep.push(rText) ; <---- save history
	rText := subStr(rText, 2)
	ReplaceString.value := rText
	; -----------
	ButUndo.Enabled := true
	TriggerChanged() 
}

GoRTrim(*) ; Trim one char from right of trigger and replacement.
{		; ----- trig -----
	tText := TriggerString.value
	tArrStep.push(tText) ; <---- save history
	tText := subStr(tText, 1, strLen(tText) - 1)
	TriggerString.value := tText
		; ----- repl -----
	rText := ReplaceString.value
	rArrStep.push(rText) ; <---- save history
	rText := subStr(rText, 1, strLen(rText) - 1)
	ReplaceString.value := rText
	; -----------
	ButUndo.Enabled := true
	TriggerChanged() 
}

; Left and Right Trims are saved in Arrays.  This function removes the last one. 
GoUndo(*)
{ 	If GetKeyState("Shift") 
		GoReStart() 
	else If (tArrStep.Length > 0) and (rArrStep.Length > 0) {
		TriggerString.value := tArrStep.Pop()
		ReplaceString.value := rArrStep.Pop()
		GoFilter()
	}
	else {
		ButUndo.Enabled := false
	}
}
; ReEnters the trigger and replacement that were gotten from RegEx upon first capture.
; Clears arrays.  Has effect of "undoing" all of the changes. 
GoReStart(*)
{ 	If !OrigTrigger and !OrigReplacment
		MsgBox("Can't restart -- Nothing in memory...")
	Else {
		TriggerString.Value := OrigTrigger ; Restore original values. 
		ReplaceString.Value := OrigReplacement
		ButUndo.Enabled := false
		tArrStep := [] ; Reset arrays to nothing.
		rArrStep := []
		GoFilter()
	}
}

; Single-click of middle radio button just calls GoFilter function but 
; double-click sets button to false.  
clickLast := 0
GoMidRadio(*)
{	global clickCurrent := A_TickCount
	if (clickCurrent - clickLast < 500) { ; Simulates watching for double-click.
		RadMid.Value := 0 ; Set middle radio to blank, and removed hotstring options. 
		MyDefaultOpts.text := strReplace(strReplace(MyDefaultOpts.text, "?", ""), "*", "")
	}
	global clickLast := A_TickCount
	GoFilter()
}

; Filters the two lists of words at bottom of Exam Pane. 
; Mostly this is called via L/R trims, or by changing radio buttons.  
; If it is called via Exam button, then reads Options box and updates radios.
GoFilter(ViaExamButt := "No", *) ; Filter the big list of words, as needed.
{	; ====== Hotstring/Trigger ===========
	tFind := Trim(TriggerString.Value)
	If !tFind
		tFind := " " ; prevents error if tFind is blank.
	tFilt := ''
	Global tMatches := 0 ; Global so I can read it in the Validation() function.
	MyOpts := MyDefaultOpts.text 

	If (ViaExamButt = "Yes") { ; Read opts box, change radios as needed.
		If  inStr(MyOpts, "*") and  inStr(MyOpts, "?")
			RadMid.value := 1
		Else if  inStr(MyOpts, "*")
			RadBeg.value := 1
		Else if  inStr(MyOpts, "?")
			RadEnd.value := 1
		Else {
			RadMid.value := 0
			RadBeg.value := 0
			RadEnd.value := 0
		}
	}

	Loop Read, WordListPath ; Compare with the big list of words and find matches.
	{
		If InStr(A_LoopReadLine, tFind) {
			IF (RadMid.value = 1) {
				tFilt .= A_LoopReadLine '`n'
				tMatches++
			}
			Else If (RadEnd.value = 1) {
				If InStr(SubStr(A_LoopReadLine, -StrLen(tFind)), tFind) {
					tFilt .= A_LoopReadLine '`n'
					tMatches++
				}
			}
			else If (RadBeg.value = 1) {
				If InStr(SubStr(A_LoopReadLine, 1, StrLen(tFind)), tFind) {
					tFilt .= A_LoopReadLine '`n'
					tMatches++
				}
			}
			Else {
				If (A_LoopReadLine = tFind) {
					tFilt := tFind
					tMatches++
				}
			}
		}
	}

	IF (RadMid.value = 1) {
		If not inStr(MyOpts, "*")
			MyOpts := MyOpts . "*"
		If not inStr(MyOpts, "?")
			MyOpts := MyOpts . "?"
	}
	Else If (RadEnd.value = 1) {
		If not inStr(MyOpts, "?")
			MyOpts := MyOpts . "?"
			MyOpts := StrReplace(MyOpts, "*")
	}
	else If (RadBeg.value = 1) {
		If not inStr(MyOpts, "*")
			MyOpts := MyOpts . "*"
			MyOpts := StrReplace(MyOpts, "?")
	}
	MyDefaultOpts.text := MyOpts

	EdtTMatches.Value := tFilt
	TxtTLable.Text := "Misspells [" . tMatches . "]"
	
	If (tMatches > 0) { 
		TrigLbl.Text := "Misspells [" . tMatches . "] words" ; Change Trig Str Label to show warning. 
		TrigLbl.SetFont("cRed")
	}
	If (tMatches = 0) {
		TrigLbl.Text := "No Misspellings found." ; Change Trig Str Label to NO LONGER show warning. 
		TrigLbl.SetFont(FontColor) ; reset color of Label, incase it's red. 
	}

	; ====== Replacement/Expansion text ==========
	rFind := Trim(ReplaceString.Value, "`n`t ")
		If !rFind
		rFind := " " ; prevents error if rFind is blank.
	rFilt := ''
	Global rMatches := 0
	
	Loop Read WordListPath  ; Compare with the big list of words and find matches.
	{
		If InStr(A_LoopReadLine, rFind) {
			IF (RadMid.value = 1) { 
				rFilt .= A_LoopReadLine '`n'

				rMatches++
			}
			Else If (RadEnd.value = 1) {
				If InStr(SubStr(A_LoopReadLine, -StrLen(rFind)), rFind) {
					rFilt .= A_LoopReadLine '`n'
					rMatches++
				}
			}
			else If (RadBeg.value = 1) { ; 'Beg' radio.
				If InStr(SubStr(A_LoopReadLine, 1, StrLen(rFind)), rFind) {
					rFilt .= A_LoopReadLine '`n'
					rMatches++
				}
			}
			Else {
				If (A_LoopReadLine = rFind) {
					rFilt := rFind
					rMatches++
				}
			}
		}
	}
	EdtRMatches.Value := rFilt
	TxtRLable.Text := "Fixes [" . rMatches . "]"
}


; ################ END of HH2 ###########################################################
; ...............................QQQ.....................QQQQQQ.....QQQ.........QQQ......
; ...............................QQQ.....................QQQQQ......QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ...............................QQQ....................QQQQ........QQQ.........QQQ......
; ..QQQQQQ....QQQQQQQQQ....QQQQQQQQQ..........QQQQQQ..QQQQQQQQ......QQQQQQQQQ...QQQQQQQQQ
; .QQQQQQQQ...QQQQQQQQQQ..QQQQQQQQQQ.........QQQQQQQQ.QQQQQQQQ......QQQQQQQQQQ..QQQQQQQQQ
; QQQQ.QQQQQ..QQQQQ.QQQQ..QQQQ.QQQQQ........QQQQ.QQQQQ..QQQQ........QQQQQ.QQQQ..QQQQQ.QQQ
; QQQ....QQQ..QQQQ...QQQ.QQQQ...QQQQ.......QQQQ....QQQ..QQQQ........QQQQ...QQQ..QQQQ...QQ
; QQQ....QQQ..QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ....QQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQQQQQQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQQQQQQQQ.QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQ..........QQQ....QQQ.QQQ.....QQQ.......QQQ.....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ.........QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQQ.QQQQ........QQQ....QQQ..QQQ....QQ
; QQQ....QQQ..QQQ....QQQ.QQQQ...QQQQ.......QQQQ....QQQ..QQQQ........QQQ....QQQ..QQQ....QQ
; QQQQ..QQQQ..QQQ....QQQ..QQQQ.QQQQQ........QQQQ.QQQQQ..QQQQ........QQQ....QQQ..QQQ....QQ
; .QQQQQQQQ...QQQ....QQQ...QQQQQQQQQ.........QQQQQQQQ...QQQQ........QQQ....QQQ..QQQ....QQ
; ..QQQQQQ....QQQ....QQQ....QQQQQQQQ..........QQQQQQ....QQQQ........QQQ....QQQ..QQQ....QQ
; #######################################################################################



;###############################################
; Number of "potential fixes" based on WordWeb app, and varies greatly by word list used. 
^F3:: ; Ctrl+F3: Report information about the autocorrect items.
StringAndFixReport(*)
{	ThisFile := FileRead(A_ScriptFullPath)
	thisOptions := '', regulars := 0, begins := 0, middles := 0, ends := 0, fixes := 0, entries := 0
	Loop Parse ThisFile, '`n'
	{	If SubStr(Trim(A_LoopField),1,1) != ':'
			continue
		entries++
		thisOptions := SubStr(Trim(A_LoopField), 1, InStr(A_LoopField, ':',,,2)) ; get options part of hotstring
		If InStr(thisOptions, '*') and InStr(thisOptions, '?')
			middles++
		Else If InStr(thisOptions, '*')
			begins++
		Else If InStr(thisOptions, '?')
			ends++
		Else
			regulars++
		If RegExMatch(A_LoopField, 'Fixes\h*\K\d+', &fn) ; Need a regex for this... 
			fixes += fn[]
	}
	MsgBox( '   Totals`n===========================`n    Regular Autocorrects:`t   ' numberFormat(regulars)
	'`n    Word Beginnings:`t`t' numberFormat(begins)
	'`n    Word Middles:`t`t' numberFormat(middles)
	'`n    Word Ends:`t`t   ' numberFormat(ends) ; this is a smaller number, so push over with '  ', simulates right justification.
	'`n===========================`n   Total Entries:`t`t' numberFormat(entries)
	'`n   Potential Fixes:`t`t' numberFormat(fixes) 
	, 'Report for ' A_ScriptName, 64
	)
	numberFormat(num) ; Function to format a number with commas (by ChatGPT4)
	{	global
		Loop 
		{	oldnum := num
			num := RegExReplace(num, "(\d)(\d{3}(\,|$))", "$1,$2") ; search for number patterns and insert commas
			if (num == oldnum) ; If the number doesn't change, exit the loop
				break
		}
		return num
	}
}
;###############################################


#Hotstring Z ; The Z causes the end char to be reset after each activation. 

;============== Determine start line of autocorrect items ======================
; Trigger String duplicate items validity check will skip lines of code before ACitemsStartAt var value.
; Gets called from Validity Check function. 
getStartLineNumber(*)
{	For idx, line in StrSplit(FileRead(A_ScriptName), "`n")
		If inStr(line, "AUTOCORRECT START POINT MARKER") { ; <--- Sees itself..  LOL 
			Global ACitemsStartAt := idx + 8 ; Should equal line number where autocorrect list starts. 
			Break ; We found it, so no need to keep looping.  
		}
	Return ACitemsStartAt
}
;===============================================================================

EDIT: How about this (below) for the logic on the "periodic log intervals"? I thought about using a temp file also, but, presumably, holding everything in RAM requires fewer hard drive read/writes. (?)

Code: Select all

#SingleInstance
#Requires AutoHotkey v2+
; timed append testing ... also append on exit ... turn off timer if no new text for 2 timer cycles. 

mygui := Gui()
thisText := mygui.addEdit('w200','')
mygui.addButton('w200','Keep Text').OnEvent('click', keepText)
mygui.Show()

logIsRunning := 0
savedUpText := ''
intervalCounter := 0  ; Initialize the counter

; There's no point running the logger if no text has been saved up...  
; So don't run timer when script starts.  Run it when logging starts. 
keepText(*)
{   If thisText.Text = ''
        return
    global savedUpText .= thisText.Text '`n'
    thisText.Text := ''
    global intervalCounter := 0  	; Reset the counter since we're adding new text
    If logIsRunning = 0  			; only start the timer it it is not already running.
        setTimer Appender, 5000  	; call function every 5 secs.
}

; Gets called by timer, or by onExit.
Appender(*) 
{   FileAppend savedUpText, "timedAppendTextLog.txt"
    global savedUpText := ''  		; clear each time, since text has been logged.
	global logIsRunning := 1  		; set to 1 so we don't keep resetting the timer.
    global intervalCounter += 1 	; Increments here, but resets in other locations. 
    If (intervalCounter > 1)  		; Check if no text has been kept for 2 intervals
    {   setTimer Appender, 0  		; Turn off the timer
        global logIsRunning := 0  	; Indicate that the timer is no longer running
        global intervalCounter := 0 ; Reset the counter for safety
    }
    SoundBeep 1200, 400
    SoundBeep 1200, 400
}

OnExit Appender 					; Also append one more time on exit. 

esc::ExitApp

setTimer debug, 20 ; just for debugging. 
debug(*)
{ 
	tooltip (
	'logIsRunning ' 		logIsRunning		
	'`nintervalCounter '	intervalCounter
	'`n`nsavedUpText' 		savedUpText
	), 100, 100
}

Any chance you'd be interested in throwing this up on GitHub? Would be nice to be able to diff it more easily and would make the process of improving the code a whole lot easier.

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 01 Mar 2024, 09:46

Jasonosaj wrote:
29 Feb 2024, 19:22
Any chance you'd be interested in throwing this up on GitHub? Would be nice to be able to diff it more easily and would make the process of improving the code a whole lot easier.
I am interested in that! I've actually had a GitHub account for a long time, and I even uploaded this project a couple of weeks ago:
https://github.com/kunkel321
But I can't figure out how to manage the files. Do you know if there are any noobie-level tutorials or how-tos that cover the things I need to know for this?
EDIT 3-2-2024: I deleted the repository, which had been added via drag n drop. I shall now experiment with GitHub Desktop (using some fake files for now).
ste(phen|ve) kunkel

xenspidey
Posts: 1
Joined: 02 Mar 2024, 23:28
Contact:

Re: AutoCorrect for v2

Post by xenspidey » 02 Mar 2024, 23:31

kunkel321 wrote:
01 Mar 2024, 09:46
Jasonosaj wrote:
29 Feb 2024, 19:22
Any chance you'd be interested in throwing this up on GitHub? Would be nice to be able to diff it more easily and would make the process of improving the code a whole lot easier.
I am interested in that! I've actually had a GitHub account for a long time, and I even uploaded this project a couple of weeks ago:
https://github.com/kunkel321
But I can't figure out how to manage the files. Do you know if there are any noobie-level tutorials or how-tos that cover the things I need to know for this?
EDIT 3-2-2024: I deleted the repository, which had been added via drag n drop. I shall now experiment with GitHub Desktop (using some fake files for now).
I'm glad I grabbed this last night before you took it down. It has gotten me a long way in getting things set up. Let me know if you need any help with github.

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 03 Mar 2024, 14:23

xenspidey wrote:
02 Mar 2024, 23:31
I'm glad I grabbed this last night before you took it down. It has gotten me a long way in getting things set up. Let me know if you need any help with github.
@xenspidey, I'm glad here hear it was useful. FYI there's a slightly more recent version in the zip here: viewtopic.php?f=83&t=120220&start=20#p559328
ste(phen|ve) kunkel

someguyinKC
Posts: 59
Joined: 25 Oct 2018, 11:33

Re: AutoCorrect for v2

Post by someguyinKC » 28 Mar 2024, 12:00

@kunkel321: i have a question for you. I am using your (beautiful and astounding) Autocorrect v2 and i have one small thing that i haven't been able to figure out.

i like to use AHK to effectively capitalize words, it is a way to save a few nanoseconds. when I use this code:

:B0X:orif::f("ORIF")

the code "works" (i hear the beep) but i can't get it to capitalize the words for me. i've tried adding and tweaking. i've read your descriptions and i understand why this is the case, but how can I turn that function off?

i even tried:

::orif::ORIF

and it didn't work. any suggestions? i also would like:

:B0X:advil::f("Advil")

others include, er --> ER and emg --> EMG

to work.

............................................................................................................

another question:

when I go through the process of using the Win+h button and click "append" to append my script, thank you for this by the way, it is really great.... but can it be set up so that the word that i made the new script for gets fixed in the original document when I click append?
thank you,

Someguy

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 28 Mar 2024, 14:09

someguyinKC wrote:
28 Mar 2024, 12:00
... i like to use AHK to effectively capitalize words, ...
Thanks for the kind words. Your situation does pose an interesting conundrum... The reason they aren't getting replaced, is because of the so called "rarification" process, as discuss above (here)
viewtopic.php?f=83&t=120220&start=20#p559621

(read below reply before proceeding)

The part of the code that does it is this:

Code: Select all

	; Rarify: Only remove and replace rightmost necessary chars.  
	trigL := StrSplit(trigger)
	replL := StrSplit(replace)
	Global ignorLen := 0
	Loop Min(trigL.Length, replL.Length) ; find matching left substring.
	{	If (trigL[A_Index] = replL[A_Index])
			ignorLen++
		else break
	}
	replace := SubStr(replace, (ignorLen+1))
	SendInput("{BS " . (TrigLen - ignorLen) . "}" replace endchar) ; Type replacemement and endchar. 
Since your trigger and replacement strings are the same, the code does zero backspaces, then types the first "zero" characters of the replacement string.

The rarification process makes sense when used as described above, but it totally messes up what you are doing there... You could try using this version of the code (I haven't actually tried it).

Code: Select all

f(replace := "") ; All the one-line autocorrects call this f(unction).
{	static HSInputBuffer := InputBuffer()
	HSInputBuffer.Start()
	trigger := A_ThisHotkey, endchar := A_EndChar
	Global lastTrigger := StrReplace(trigger, "X", "") "::" replace ; set 'lastTrigger' before removing options and colons.
	SendInput(replace endchar) ; Type replacemement and endchar. 
	replace := "" ; Reset to blank string.
	HSInputBuffer.Stop()
	SoundBeep(900, 60) ; Notification of replacement.
	addToLog := LastTrigger  "`n"
	GoLogger(addToLog) ; Uses same logger function as above CAse COrrector. 
}
IMPORTANT: The autocorrect items all have B0 (B zero) in the hotstring options. You'll need to remove all of those, so that AutoHotkey uses its default behavior of auto-backspacing the triggers.

A relevant consideration though: Check out Descolada's ::trigger::_HS("replacement") function here:
viewtopic.php?p=565361#p565361
His has a built-in option to turn off the auto-backspacing, which is exactly what you need. The only downside is that his doesn't have the logging feature.

Last thing I'll point out: In my own autocorrect library, not all of my hotstrings use the f() function... It is only so I can log my accidental typo corrections. Other boilerplate template items just use plain vanilla hotstings. They can be put in the same ahk file... Just don't include the f() function call. You could do that for your capilizing items. If you do, I'd recommend the 'case sensitive' option, like this:

Code: Select all

:C:orif::ORIF
That will cause AHK to ignore when It's already typed in caps... Only typing it in lower-case will trigger the string.
ste(phen|ve) kunkel

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 28 Mar 2024, 19:00

@someguyinKC Building on the previous reply... It's 5 hours later and the solution just now occurred to me. Your situation can be accommodated with a single character! Find the rarification code that looks like this:

Code: Select all

If (trigL[A_Index] = replL[A_Index])
and make a double-equals, like this:

Code: Select all

If (trigL[A_Index] == replL[A_Index])
This will force the comparison to be case-sensitive, and the original "rarification" functionality of the function is retained.

For the reason described in the last post, I still recommend a C in the options. Like:

Code: Select all

:B0XC:orif::f("ORIF")
:B0XC:advil::f("Advil")
:D
ste(phen|ve) kunkel

Descolada
Posts: 1141
Joined: 23 Dec 2021, 02:30

Re: AutoCorrect for v2

Post by Descolada » 29 Mar 2024, 03:14

@kunkel321, @Jasonosaj, I've written an experimental HotstringRecognizer class which could be used to detect the case of the trigger word. I've also modified the InputBuffer class a bit to account for modifier keys pressed down when the buffering is activated.

Please notify me if you find any oddities/bugs with it :)

Code: Select all

#requires AutoHotkey v2
; #MaxThreadsPerHotkey needs to be higher than 1, otherwise some hotstrings might get lost 
; if their activation strings were buffered.
#MaxThreadsPerHotkey 10
; Enable X (execution) and B0 (no backspacing) for all hotstrings, which are necessary for this library to work.
; Z option resets the hotstring recognizer after each replacement, as AHK does with auto-replace hotstrings
#Hotstring ZXB0

; For demonstration purposes lets use SendEvent as the default hotstring mode.
; This can't be enabled with `#Hotstring SE` because that setting is not accessible from AHK.
; O (omit EndChar) argument needs to be changed with this AND with `#Hotstring O`.
_HS(, "SE")

; Regular hotstrings only need to be wrapped with _HS. This uses SendEvent with default delay 0.
::fwi::_HS("for your information")
; Regular hotstring arguments can be used; this sets the keydelay to 40ms (this works since we are using SendMode Event)
:K40:afaik::_HS("as far as I know")
; Other hotstring arguments can be used as well such as Text
:T:omg::_HS("oh my god{enter}")
; Backspacing can be limited to n backspaces with Bn
:*:because of it's::_HS("s", "B2")
; ... however that's usually not necessary, because unlike the default implementation, this one backspaces 
; only the non-matching end of the trigger string
:*:thats::_HS("that's")
; To use regular hotstrings without _HS, reverse the global changes locally (X0 disables execute, B enables backspacing)
:X0B:btw::by the way

/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstring, 1, 1), 'Locale') {
        if StrLen(EndChar)
            ThisHotstring := SubStr(ThisHotstring, 1, -1)
        if IsUpper(RegExReplace(SubStr(ThisHotstring, 2), "\W"), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_")) + !Omit

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((this, value) => this.MinSendLevel := value).Bind(this.__Hook), 
            get:((this) => this.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, value) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.Content := SubStr(this.Content, 1, -1) : this.Content := "", this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.Content := ""  
        this.Content .= char, this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}
Last edited by Descolada on 29 Mar 2024, 09:57, edited 1 time in total.

someguyinKC
Posts: 59
Joined: 25 Oct 2018, 11:33

Re: AutoCorrect for v2

Post by someguyinKC » 29 Mar 2024, 07:30

kunkel321 wrote:
28 Mar 2024, 19:00
@someguyinKC Building on the previous reply... It's 5 hours later and the solution just now occurred to me. Your situation can be accommodated with a single character! Find the rarification code that looks like this:

Code: Select all

If (trigL[A_Index] = replL[A_Index])
and make a double-equals, like this:

Code: Select all

If (trigL[A_Index] == replL[A_Index])
This will force the comparison to be case-sensitive, and the original "rarification" functionality of the function is retained.

For the reason described in the last post, I still recommend a C in the options. Like:

Code: Select all

:B0XC:orif::f("ORIF")
:B0XC:advil::f("Advil")
:D
@kunkel321

HOLEY MOLEY THIS IS GREAT!!!!

ONE character fixed the problem! amazing. Thank you so much for all you are doing here. you are making life better for me (and surely others)!!
thank you,

Someguy

Jasonosaj
Posts: 51
Joined: 02 Feb 2022, 15:02
Location: California

Re: AutoCorrect for v2

Post by Jasonosaj » 29 Mar 2024, 10:21

Every once in a while (actually, a lot more frequently than that) I am reminded that I am merely tinkering around the edves of work done by people that know what the hell they are doing. THIS is one of those moments. Thanks so much, @Descolda. Will report any issues and review to try to understand how you did this! Comments look SUPER helpful.
Descolada wrote:
29 Mar 2024, 03:14
@kunkel321, @Jasonosaj, I've written an experimental HotstringRecognizer class which could be used to detect the case of the trigger word. I've also modified the InputBuffer class a bit to account for modifier keys pressed down when the buffering is activated.

Please notify me if you find any oddities/bugs with it :)

Code: Select all

#requires AutoHotkey v2
; #MaxThreadsPerHotkey needs to be higher than 1, otherwise some hotstrings might get lost 
; if their activation strings were buffered.
#MaxThreadsPerHotkey 10
; Enable X (execution) and B0 (no backspacing) for all hotstrings, which are necessary for this library to work.
; Z option resets the hotstring recognizer after each replacement, as AHK does with auto-replace hotstrings
#Hotstring ZXB0

; For demonstration purposes lets use SendEvent as the default hotstring mode.
; This can't be enabled with `#Hotstring SE` because that setting is not accessible from AHK.
; O (omit EndChar) argument needs to be changed with this AND with `#Hotstring O`.
_HS(, "SE")

; Regular hotstrings only need to be wrapped with _HS. This uses SendEvent with default delay 0.
::fwi::_HS("for your information")
; Regular hotstring arguments can be used; this sets the keydelay to 40ms (this works since we are using SendMode Event)
:K40:afaik::_HS("as far as I know")
; Other hotstring arguments can be used as well such as Text
:T:omg::_HS("oh my god{enter}")
; Backspacing can be limited to n backspaces with Bn
:*:because of it's::_HS("s", "B2")
; ... however that's usually not necessary, because unlike the default implementation, this one backspaces 
; only the non-matching end of the trigger string
:*:thats::_HS("that's")
; To use regular hotstrings without _HS, reverse the global changes locally (X0 disables execute, B enables backspacing)
:X0B:btw::by the way

/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstring, 1, 1), 'Locale') {
        if StrLen(EndChar)
            ThisHotstring := SubStr(ThisHotstring, 1, -1)
        if IsUpper(RegExReplace(SubStr(ThisHotstring, 2), "\W"), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_")) + !Omit

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((this, value) => this.MinSendLevel := value).Bind(this.__Hook), 
            get:((this) => this.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, value) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.Content := SubStr(this.Content, 1, -1) : this.Content := "", this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.Content := ""  
        this.Content .= char, this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 29 Mar 2024, 12:33

Like Jason said... This is amazing! Thanks for making and sharing it! Also like Jason said, it is well beyond my coding abilities -- LOL.

Some comments:
The only error message I was able to get was the one on the bottom line of code here.

Code: Select all

::cteve/::_HS("steve") ; works:  CTEVE/ changed to STEVE
::;cte::_HS("case text experiments") ; leading char breaks it... ;CTE changed to "case text experiments"
:X0B:;hse::hot string experiments ; but... ;HSE is changed to "HOT STRING EXPERIMENTS"

#!u::MsgBox 'content: ' HotstringRecognizer.Content
#!i::MsgBox 'length: ' HotstringRecognizer.Length
#!o::MsgBox 'isActive: ' HotstringRecognizer.IsActive
#!p::MsgBox 'inSndLvl: ' HotstringRecognizer.MinSendLevel ; <-----   "Error: Too many parameters passed to function."
She also bottom hotstring. Personally, I like to have a "prefix" character for my "acronym expansion" hotstrings. I've seen other folks use a slash at the end.

AHK's default hotstring recognizer will disregard the semicolon prefix, and use the first occurring [A-Z] character for the case confirmation. It might be nice to add that functionality. The ending slash suffix one already works. :)

fyi in the comments, you have HotstringRecognizer.ResetKeys as a Property, but is it actually a Method? IDK

In an Attempt to wrap my head around how to implement the class, I attempted a super-barebones use:

Code: Select all

; Steve's attempted barebones implementation of the HSRec Class. 
:X:tbd::funct("to be determined")
funct(replacement?) {
    static __Init := HotstringRecognizer.Start()
    Send HotstringRecognizer.Content 
    HotstringRecognizer.Reset()
}
Am I even close to using it correctly? :lol:
EDIT: It just occurred to me that I'm not using the replacement variable anywhere... So I'm not sure how I expect this to do anything. :facepalm:
Last edited by kunkel321 on 29 Mar 2024, 12:41, edited 1 time in total.
ste(phen|ve) kunkel

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 29 Mar 2024, 12:38

someguyinKC wrote:
29 Mar 2024, 07:30
ONE character fixed the problem! amazing. Thank you so much for all you are doing here. you are making life better for me (and surely others)!!
You are very welcome!
Question: Do you have a solid state drive (SSD) or a spinning hard drive? If you have a spinning drive, how do you feel about the logger in the f() function writing to your disc every time there's a correction? I had a suggestion to cache them, then only write to disc periodically. I plan to implement it... Just haven't done it yet. Do you think it will be helpful? I have a SSD, so I'm not sure it will make much difference to me personally.
ste(phen|ve) kunkel

Descolada
Posts: 1141
Joined: 23 Dec 2021, 02:30

Re: AutoCorrect for v2

Post by Descolada » 29 Mar 2024, 15:17

@kunkel321, apparently AHK doesn't consider only letters [A-Z], but it considers any locale-specific letters as well. It is possible to implement the same method that AHK internally uses, but for simplicity sake I opted to use a regex alternative instead which should give almost the same result.

The error with HotstringRecognizer.MinSendLevel has been now fixed.

HotstringRecognizer.ResetKeys is a property, and by default it contains the keys that, when pressed, reset the recognizer content. From the docs for Hotstrings:
Any backspacing you do is taken into account for the purpose of detecting hotstrings. However, the use of ↑, →, ↓, ←, PgUp, PgDn, Home, and End to navigate within an editor will cause the hotstring recognition process to reset. In other words, it will begin waiting for an entirely new hotstring.
The following contains all the fixes/improvements, and also adds a HotstringRecognizer OnChange event handler so you can see what is going on in a ToolTip:

Code: Select all

#requires AutoHotkey v2
; #MaxThreadsPerHotkey needs to be higher than 1, otherwise some hotstrings might get lost 
; if their activation strings were buffered.
#MaxThreadsPerHotkey 10
; Enable X (execution) and B0 (no backspacing) for all hotstrings, which are necessary for this library to work.
; Z option resets the hotstring recognizer after each replacement, as AHK does with auto-replace hotstrings
#Hotstring ZXB0

; For demonstration purposes lets use SendEvent as the default hotstring mode.
; This can't be enabled with `#Hotstring SE` because that setting is not accessible from AHK.
; O (omit EndChar) argument needs to be changed with this AND with `#Hotstring O`.
_HS(, "SE")

HotstringRecognizer.OnChange := (OldContent, NewContent, *) => ToolTip("Previous content: " OldContent "`nNew content: " NewContent)

; Regular hotstrings only need to be wrapped with _HS. This uses SendEvent with default delay 0.
::fwi::_HS("for your information")
; Regular hotstring arguments can be used; this sets the keydelay to 40ms (this works since we are using SendMode Event)
:K40:afaik::_HS("as far as I know")
; Other hotstring arguments can be used as well such as Text
:T:omg::_HS("oh my god{enter}")
; Backspacing can be limited to n backspaces with Bn
:*:because of it's::_HS("s", "B2")
; ... however that's usually not necessary, because unlike the default implementation, this one backspaces 
; only the non-matching end of the trigger string
:*:thats::_HS("that's")
; To use regular hotstrings without _HS, reverse the global changes locally (X0 disables execute, B enables backspacing)
:X0B:btw::by the way

/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale') {
        if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_")) + !Omit

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 
            get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this, *) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    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"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.__SetContent("")  
        this.__SetContent(this.Content char), this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
    static __SetContent(Value) {
        if this.OnChange && this.Content !== Value
            SetTimer(this.OnChange.Bind(this.Content, Value), -1)
        this.Content := Value
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}

User avatar
kunkel321
Posts: 1061
Joined: 30 Nov 2015, 21:19

Re: AutoCorrect for v2

Post by kunkel321 » 29 Mar 2024, 15:52

Descolada wrote:
29 Mar 2024, 15:17
... The following contains all the fixes/improvements, ...
Awesome! Works like a charm. I'd say this baby is ready for prime time! :D
ste(phen|ve) kunkel

Post Reply

Return to “Scripts and Functions (v2)”