You're awesome. Wish I had just asked!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.
AutoCorrect for v2
Re: AutoCorrect for v2
Re: AutoCorrect for v2
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.
Re: AutoCorrect for v2
@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?
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
@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.
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.
Re: AutoCorrect for v2
@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.
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.
Re: AutoCorrect for v2
@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.
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. (?)
@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
}
;===============================================================================
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
}
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
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.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.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 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 } ;===============================================================================
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 }
Re: AutoCorrect for v2
I am interested in that! I've actually had a GitHub account for a long time,
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).
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
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.kunkel321 wrote: ↑01 Mar 2024, 09:46I 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).
Re: AutoCorrect for v2
@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
name := "ste(phen|ve) kunkel"
-
- Posts: 70
- Joined: 25 Oct 2018, 11:33
Re: AutoCorrect for v2
@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?
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
Someguy
Re: AutoCorrect for v2
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.
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.
}
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
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
@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:
and make a double-equals, like this:
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
If (trigL[A_Index] = replL[A_Index])
Code: Select all
If (trigL[A_Index] == replL[A_Index])
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")
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
@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
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.
-
- Posts: 70
- Joined: 25 Oct 2018, 11:33
Re: AutoCorrect for v2
@kunkel321kunkel321 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: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.Code: Select all
If (trigL[A_Index] == replL[A_Index])
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")
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
Someguy
Re: AutoCorrect for v2
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 }
Re: AutoCorrect for v2
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.
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:
Am I even close to using it correctly?
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.
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."
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()
}
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.
Last edited by kunkel321 on 29 Mar 2024, 12:41, edited 1 time in total.
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
You are very welcome!someguyinKC wrote: ↑29 Mar 2024, 07:30ONE 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)!!
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.
name := "ste(phen|ve) kunkel"
Re: AutoCorrect for v2
@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:
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:
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: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.
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
}
Re: AutoCorrect for v2
Awesome! Works like a charm. I'd say this baby is ready for prime time!
name := "ste(phen|ve) kunkel"
Return to “Scripts and Functions (v2)”
Who is online
Users browsing this forum: No registered users and 33 guests