Find dialog with whole word and RegEx support

Post your working scripts, libraries and tools
User avatar
jeeswg
Posts: 5155
Joined: 19 Dec 2016, 01:58
Location: UK

Find dialog with whole word and RegEx support

07 Jun 2018, 23:38

- Here is a script to replace Notepad's Find dialog with a more powerful Find dialog. It overrides the Ctrl+F and F3 hotkeys. The window criteria can be changed to make the script work on other programs that use Edit controls.
- The script is an AHK v2 script, rather than an AHK v1.1 script, because AHK v2 is much better for producing GUIs.

Code: Select all

;AHK v2

;Find dialog with whole word and RegEx support
;note: alt+down to display the drop-down list

#SingleInstance force
#KeyHistory 0
#UseHook
ListLines("Off")
A_TrayMenu.ClickCount := 1

#Persistent
;#NoTrayIcon
DetectHiddenWindows("On")
SplitPath(A_ScriptName,,,, vScriptNameNoExt)
A_IconTip := vScriptNameNoExt

;options:
;set the Find dialog window size:
;vZoom := 1.8
vZoom := 1.5
;in a 'whole' match, characters not considered boundary characters:
vCharList := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
;shift+enter to search upwards:
vDoShiftEnter := 1
;when do search, hide box or maintain box:
vDoHideWhenSearch := 0
;if needle not found, move caret to start/end and retry:
vDoRetry := 1
;determine target window
GroupAdd("WinGroupFind", "ahk_class Notepad")

vNeedle := ""
vWhole := 0
vCaseSen := 0
vUp := 0
vDown := 1

;==================================================

;WINDOW=X0 Y0 W354 H121 ;originally: H101
vList := " ;continuation section
(C
Text=X6 Y13 W63 H13
Edit=X71 Y11 W192 H20
Checkbox=X6 Y42 W150 H20
Checkbox=X6 Y68 W96 H20
GroupBox=X161 Y42 W102 H46
Radio=X167 Y62 W38 H20
Radio=X207 Y62 W53 H20
Button=X273 Y8 W75 H23
Button=X273 Y37 W75 H23
Button=X273 Y73 W75 H23
Checkbox=X6 Y94 W150 H20 ;I added this control
)"
vList := RegExReplace(vList, "`am)[XYWH](?!.*=)")

oPos := []
Loop Parse, vList, "`n"
{
	oTemp := StrSplit(A_LoopField, ["=", " "])
	oTemp.RemoveAt(1)
	Loop 4
		oTemp[A_Index] *= vZoom
	vCtlPos := Format("X{} Y{} W{} H{}", oTemp*)
	oPos.Push(vCtlPos)
}

oGui := GuiCreate("-MinimizeBox", "Find")
hGui := oGui.hWnd
oGui.OnEvent("Close", "Gui_Close")
oGui.OnEvent("Escape", "OnCancel")
oGui.SetFont("s18", "Arial")

;12 controls: Static, ComboBox+Edit, 9 Buttons
;BS_MULTILINE := 0x2000
;WS_EX_NOPARENTNOTIFY := 0x4
oGui.Add("Text", oPos.1 " +E0x4 +Group", "Fi&nd what:")
oCbx := oGui.Add("ComboBox", oPos.2 " +E0x4 +Group R8")
hCbx := oCbx.hWnd
hEdit := ControlGetHwnd("Edit1", "ahk_id " hCbx)
oEdit := GuiCtrlFromHwnd(hEdit)
oBtnW := oGui.Add("Checkbox", oPos.3 " -0x2000 +E0x4 +Group", "Match &whole word only")
oBtnC := oGui.Add("Checkbox", oPos.4 " -0x2000 +E0x4", "Match &case")
oBtnG := oGui.Add("GroupBox", oPos.5 " -0x2000 +E0x4 +Group", "Direction")
oBtnU := oGui.Add("Radio", oPos.6 " -0x2000 +E0x4 +Group", "&Up")
oBtnD := oGui.Add("Radio", oPos.7 " -0x2000 +E0x4 +Checked", "&Down")
oBtnF := oGui.Add("Button", oPos.8 " -0x2000 +E0x4 +Group", "&Find Next")
oBtnX := oGui.Add("Button", oPos.9 " -0x2000 +E0x4", "Cancel")
oBtnH := oGui.Add("Button", oPos.10 " -0x2000 +E0x4 +Hidden +Disabled", "&Help")
oBtnR := oGui.Add("Checkbox", oPos.11 " -0x2000 +E0x4", "Match &RegEx")
oBtnF.OnEvent("Click", "OnSearch")
oBtnR.OnEvent("Click", "OnCheckRegEx")
oBtnW.OnEvent("Click", "OnCheckWhole")
oBtnX.OnEvent("Click", "OnCancel")
oBtnF.Opt("+Default")
;note: oBtnG/oBtnH/hBtnG/hBtnH not used by script
Loop Parse, "CDFGHRUWX"
	hBtn%A_LoopField% := oBtn%A_LoopField%.hWnd

;remove menu items (Restore/Size/Minimize/Maximize/separator):
;keep only Move/Close menu items
;SC_RESTORE := 0xF120 ;SC_MOVE := 0xF010
;SC_SIZE := 0xF000 ;SC_MINIMIZE := 0xF020
;SC_MAXIMIZE := 0xF030 ;SC_CLOSE := 0xF060
hSysMenu := DllCall("GetSystemMenu", Ptr,hGui, Int,0, Ptr)
vList := "0xF120,0xF000,0xF020,0xF030"
Loop Parse, vList, ","
	DllCall("user32\RemoveMenu", Ptr,hSysMenu, UInt,A_LoopField, UInt,0x0)
DllCall("user32\RemoveMenu", Ptr,hSysMenu, UInt,1, UInt,0x400) ;MF_BYPOSITION := 0x400 ;remove separator
OnMessage(0x100, "OnKeyDown") ;WM_KEYDOWN := 0x100

oSearchHist := []
return

;==================================================

#If WinActive("ahk_group WinGroupFind")
^f:: ;show Find dialog
hWndMain := WinGetID("ahk_group WinGroupFind")
if JEE_WinIsVisible(hGui)
{
	WinActivate("ahk_id " hGui)
	hWndMainLast := hWndMain
	return
}

if !(hWndMainLast = hWndMain)
{
	;check Down, uncheck Up
	PostMessage(0xF1, 1, 0,, "ahk_id " hBtnD) ;BM_SETCHECK := 0xF1
	PostMessage(0xF1, 0, 0,, "ahk_id " hBtnU) ;BM_SETCHECK := 0xF1
}

ControlFocus("Edit1", "ahk_id " hGui)
PostMessage(0xB1, 0, -1,, "ahk_id " hEdit) ;EM_SETSEL := 0xB1 ;select all
;WinActivate("ahk_id " hGui)
;WinShow("ahk_id " hGui)
;oGui.Show()
;oGui.Show("X202 Y283")
oGui.Show("X202 Y325")
hWndMainLast := hWndMain
return

;==================================================

F3:: ;repeat last search
hWndMain2 := WinGetID("ahk_group WinGroupFind")
hCtlMain2 := ControlGetHwnd("Edit1", "ahk_id " hWndMain2)
if vDoHideWhenSearch
{
	oGui.Hide()
	WinActivate("ahk_id " hWndMain2)
	ControlFocus(, "ahk_id " hCtlMain2)
}
vNeedle := ControlGetText(, "ahk_id " hEdit)
if (vNeedle = "")
	return
vWhole := ControlGetChecked(, "ahk_id " hBtnW)
vCaseSen := ControlGetChecked(, "ahk_id " hBtnC)
;vUp := ControlGetChecked(, "ahk_id " hBtnU)
vDown := ControlGetChecked(, "ahk_id " hBtnD)
vRegEx := ControlGetChecked(, "ahk_id " hBtnR)
vNormal := !vWhole && !vRegEx

;note: repeated options e.g. 'ii)' are permitted
if vRegEx
	if vCaseSen
		vNeedle2 := vNeedle
	else if RegExMatch(vNeedle, "^[A-Za-z`a`n`r `t]*\K\)")
		vNeedle2 := "i" vNeedle
	else
		vNeedle2 := "i)" vNeedle

if vIsRetry
	vPos1 := vPos2 := vDown ? 0 : vLen, vIsRetry := 0
else
	JEE_EditGetSel(hCtlMain2, vPos1, vPos2)
vPos1 += 2 ;convert to 1-based, add 1, so +1+1
vPos2 -= 1 ;convert to 1-based, subtract 2, so +1-2
vText := ControlGetText(, "ahk_id " hCtlMain2)

vLen := StrLen(vText)
vLenN := StrLen(vNeedle)

if (vNormal || vWhole) && (vLenN > vLen)
	return

vPos := ""
if vNormal && vDown
	vPos := InStr(vText, vNeedle, vCaseSen, vPos1)
else if vNormal && !vDown
	vPos := InStr(vText, vNeedle, vCaseSen, vPos2-vLen-1) ;AHK v2: -1 = len-len-1 = last char
else if vWhole && vDown
	vPos := JEE_InStrWhole(vText, vNeedle, vCaseSen, vPos1, 1, vCharList)
else if vWhole && !vDown
	vPos := JEE_InStrWhole(vText, vNeedle, vCaseSen, vPos2-vLen-1, 1, vCharList)
else if vRegEx && vDown
	try vPos := JEE_RegExMatch(vText, vNeedle2, oMatch, vPos1)
else if vRegEx && !vDown
	try vPos := JEE_RegExMatchLast(SubStr(vText, 1, vPos1-1), vNeedle2, oMatch)

if vRegEx && (vPos = "")
{
	MsgBox("error: invalid RegEx")
	return
}

if vPos
{
	vLenN := vRegEx ? StrLen(oMatch.0) : StrLen(vNeedle)
	WinActivate("ahk_id " hWndMain2)
	SendMessage(0xB1, vPos-1, vPos-1+vLenN,, "ahk_id " hCtlMain2) ;EM_SETSEL := 0xB1
	PostMessage(0xB7, 0, 0,, "ahk_id " hCtlMain2) ;EM_SCROLLCARET := 0xB7
	if !InStr(A_ThisHotkey, "F3")
	&& !vDoHideWhenSearch
		WinActivate("ahk_id " hGui)
}
else if !vDoRetry
	MsgBox("text not found:`r`n" vNeedle)
else
{
	vRet := MsgBox("start again from " (vDown ? "top" : "bottom") " of file?",, "YNC")
	if (vRet = "Yes")
	{
		vIsRetry := 1
		Gosub F3
	}
	else
		oGui.Hide()
}
return

;==================================================

;JEE_WinVisible
;JEE_WinGetVisible
JEE_WinIsVisible(hWnd)
{
	if DllCall("user32\IsWindowVisible", Ptr,hWnd) ;1 = visible, 0 = not visible
		return 1
	else
		return 0
}

;==================================================

;where vPos1 and vPos2 are 0-based
;e.g. JEE_EditGetSel(hCtl, vPos1, vPos2)
;vPos1=left, vPos2=right
;JEE_EditGetRange
JEE_EditGetSel(hCtl, ByRef vPos1, ByRef vPos2)
{
	VarSetCapacity(vPos1, 4, 0), VarSetCapacity(vPos2, 4, 0)
	SendMessage(0xB0, &vPos1, &vPos2,, "ahk_id " hCtl) ;EM_GETSEL := 0xB0 ;(left, right)
	vPos1 := NumGet(&vPos1, 0, "UInt"), vPos2 := NumGet(&vPos2, 0, "UInt")
}

;==================================================

;where vPos1 and vPos2 are 0-based
;e.g. JEE_EditSetSel(hCtl, vPos1, vPos2)
;vPos1=anchor, vPos2=active
;JEE_EditSetRange
JEE_EditSetSel(hCtl, vPos1, vPos2, vDoScroll:=0)
{
	SendMessage(0xB1, vPos1, vPos2,, "ahk_id " hCtl) ;EM_SETSEL := 0xB1 ;(anchor, active)
	if vDoScroll
		SendMessage(0xB7, 0, 0,, "ahk_id " hCtl) ;EM_SCROLLCARET := 0xB7
}

;==================================================

OnCancel()
{
	global hGui
	WinHide("ahk_id " hGui)
}

OnCheckRegEx()
{
	global hBtnW
	;note: RegEx is toggled automatically
	;uncheck Whole
	PostMessage(0xF1, 0, 0,, "ahk_id " hBtnW) ;BM_SETCHECK := 0xF1
}

OnCheckWhole()
{
	global hBtnR,
	;note: Whole is toggled automatically
	;uncheck RegEx
	PostMessage(0xF1, 0, 0,, "ahk_id " hBtnR) ;BM_SETCHECK := 0xF1
}

;Ctrl+F: prevent beep, focus Edit control
;Shift+Enter: search upwards
OnKeyDown(wParam, lParam, uMsg, hWnd) ;WM_KEYDOWN := 0x100
{
	global hBtnD, hBtnU, hEdit, hGui, vDoShiftEnter
	if (wParam = 0x46) ;F
	&& !GetKeyState("Shift")
	&& GetKeyState("Ctrl")
	&& !GetKeyState("LWin")
	&& !GetKeyState("RWin")
	&& !GetKeyState("Alt")
	{
		WinActivate("ahk_id " hGui)
		ControlFocus(, "ahk_id " hEdit)
		return 0
	}
	if vDoShiftEnter
	&& (wParam = 0xD) ;VK_RETURN := 0xD ;Enter
	&& GetKeyState("Shift")
	&& !GetKeyState("Ctrl")
	&& !GetKeyState("LWin")
	&& !GetKeyState("RWin")
	&& !GetKeyState("Alt")
	{
		;check Up, uncheck Down
		SendMessage(0xF1, 1, 0,, "ahk_id " hBtnU) ;BM_SETCHECK := 0xF1
		PostMessage(0xF1, 0, 0,, "ahk_id " hBtnD) ;BM_SETCHECK := 0xF1
		Gosub F3
		return 0
	}
}

OnSearch()
{
	global hCbx, oCbx, oSearchHist
	;vNeedle := ControlGetText(, "ahk_id " hCbx)
	vNeedle := oCbx.Text
	if (vNeedle = "")
		return
	for vKey, vValue in oSearchHist
		if (vValue = vNeedle)
		{
			vKeyDel := vKey
			break
		}
	if !(vKeyDel = "")
		oSearchHist.RemoveAt(vKeyDel)
	oSearchHist.InsertAt(1, vNeedle)
	oCbx.Delete()
	oCbx.Add(oSearchHist)
	oCbx.Value := 1
	Gosub F3
}

Gui_Close()
{
	;ExitApp()
}

;==================================================

JEE_InStrWhole(vText, vNeedle, vCaseSen:=0, vPos:=1, vOcc:=1, vCharList:="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
{
	static vIsV1 := !!InStr(1, 1, 1, 0)
	if (vPos = 0)
		return
	if vIsV1 && (vPos <= -1)
		vPos++
	if (vPos > 1)
		while vPos := InStr(vText, vNeedle, vCaseSen, vPos)
		{
			vChar2 := SubStr(vText, vPos+StrLen(vNeedle), 1)
			if ((vPos = 1) || !InStr(vCharList, SubStr(vText, vPos-1, 1)))
			&& ((vChar2 = "") || !InStr(vCharList, vChar2))
				return vPos
			vPos++
		}
	else ;handle negative offset
		while vPos := InStr(vText, vNeedle, vCaseSen, vPos)
		{
			vChar2 := SubStr(vText, vPos+StrLen(vNeedle), 1)
			if ((vPos = 1) || !InStr(vCharList, SubStr(vText, vPos-1, 1)))
			&& ((vChar2 = "") || !InStr(vCharList, vChar2))
				return vPos
			vPos++
		}
	return 0
}

;==================================================

;MsgBox, % JEE_RegExMatchLast("aaaaaaaaaabbbbb", "a") ;10
;MsgBox, % JEE_RegExMatchLast("a`r`na`r`na", "a") ;7
;MsgBox, % JEE_RegExMatchLast("a`ra`ra", "a") ;5
;MsgBox, % JEE_RegExMatchLast("a`na`na", "a") ;5

;works like RegExMatch (AHK v2) (on AHK v1/v2)
JEE_RegExMatchLast(ByRef vText, vNeedle, ByRef oArray:="", vPos:=1)
{
	if vPosX := RegExMatch(vNeedle, "^[A-Za-z`a`n`r `t]*\K\)")
		vOpt := SubStr(vNeedle, 1, vPosX-1), vNeedle := SubStr(vNeedle, vPosX+1)
	if !InStr(vOpt, "s", 1)
		vOpt := "s" vOpt
	return JEE_RegExMatch(vText, (vOpt = "" ? "" : vOpt ")") ".*\K" vNeedle, oArray, vPos)
}

;==================================================

;works like RegExMatch (AHK v2) (on AHK v1/v2)
JEE_RegExMatch(ByRef vText, vNeedle, ByRef oArray:="", vPos:=1)
{
	;we're not interested in the return value, but we need it to get RegExMatch to take place
	static vRet := RegExMatch("", "", o), vIsV1 := !IsObject(o)
	if !vIsV1
		return RegExMatch(vText, vNeedle, oArray, vPos)
	if (vPos = 0)
		return
	else if (vPos <= -1)
		vPos++
	if vPosX := RegExMatch(vNeedle, "^[A-Za-z`a`n`r `t]*\K\)")
		vOpt := "O" SubStr(vNeedle, 1, vPosX-1), vNeedle := SubStr(vNeedle, vPosX+1)
	else
		vOpt := "O"
	if !RegExMatch(vOpt, "[`r`n`a]")
		vNeedle := "(*ANYCRLF)(*BSR_ANYCRLF)" vNeedle
	return RegExMatch(vText, vOpt ")" vNeedle, oArray, vPos)
}

;==================================================
Last edited by jeeswg on 12 Jun 2018, 07:28, edited 1 time in total.
Helgef
Posts: 3200
Joined: 17 Jul 2016, 01:02
Contact:

Re: Find dialog with whole word and RegEx support

11 Jun 2018, 10:43

Very nice jeeswg, it works well. I would probably catch regex exceptions and display the error in the search dialog, maybe color the needle red when it is invalid.

Thanks for sharing, cheers.
User avatar
jeeswg
Posts: 5155
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Find dialog with whole word and RegEx support

12 Jun 2018, 07:39

- Thanks for testing Helgef. I've added in error handling for RegEx.
- I've set the zoom to 1.5x and to the position that I myself use. The previous values (still available as comments) were 1.8x (to display the text without truncation) and centred (the default when no position is specified).
- I've made the buttons single-line (as in an older prototype script), this makes them look better when the text is too big for the Button control. (It prevents vertical centring and wraparound.)

- I just used the script for the first time, I wanted to do a RegEx search for \d:\d.
- I had been using a very similar script, that lacked the RegEx handling. However, that script used my unfinished custom GUI function library, and so wasn't going to be ready to share for a while. It had the advantage of having no icon. It also had the advantage of having a custom class name, #32770, it was thus compatible with existing hotkeys that I'd defined for the Notepad Find dialog.
- This script flashes each time it's used on Notepad, although I don't notice this when it's of a certain position and size. I had wondered about temporarily changing the window parent/owner, in case this could avoid the flash.
- I presume that you wouldn't get the flash, if you used this script as part of your own GUI, e.g. for a custom Notepad replacement.
Guest

Re: Find dialog with whole word and RegEx support

12 Jun 2018, 07:41

How about a screenshot for those who can't bothered to download v2 just to have a peek at how it looks :-)

Return to “Scripts and Functions”

Who is online

Users browsing this forum: No registered users and 24 guests