Interface to choose a list item by typing, with autocompletion

A user-interface to select an item from a list, using keyboard entry to refine the selection options.

I used a notebook subwindow to create a control-like interface. Typing in the subwindow refines the list of possible matches within the provided list of values. The matches are displayed in a listbox. Autocompletion suggestions are shown as grey text. The tab key accepts the current autocomplete suggestion, and the up and down arrows step through the matches. The "Do It" button passes the notebook text on to the doSomething() function for further processing.

Execute GUIdemo() to create an example interface using the output from FunctionList to generate a string list, or pass your own list to ChooseFromListGUI(strList).

 

#pragma TextEncoding="UTF-8"
#pragma rtGlobals=3
#pragma IgorVersion=8
#pragma version=1.33
#pragma ModuleName=ListFilter
#include <Resize Controls>

// https: www.wavemetrics.com/user/tony

// ----------------  edit this section ---------------------

// edit this function to do something useful with selection
static function doSomething(string selection)
	DoAlert 0, "You selected " + selection
end

// edit this function to choose what to do with a double click
static function onDoubleClick(string selection)
	doSomething(selection)
end

// execute ChooseFromListGUI(strList) to create the GUI
function GUIdemo()
	string strList = FunctionList("*", ";", "KIND:1")
	ChooseFromListGUI(strList, title="List Selector Demo")
end
// ----------------------------------------------------------

function ChooseFromListGUI(string strList, [string title])
	title = SelectString(ParamIsDefault(title), title, "")
	
	// killing any old window also clears package data folder
	KillWindow/Z FilterPanel
	
	// create package data folder
	NewDataFolder/O root:Packages
	NewDataFolder/O root:Packages:ChooseFromListGUI
	DFREF dfr = root:Packages:ChooseFromListGUI
		
	// create a sorted text wave
	wave w = ListToTextWave(strList, ";")
	Sort w, w
	Duplicate/O w dfr:displayList /WAVE=displayList, dfr:fullList
		
	// make a control panel GUI
	NewPanel/K=1/W=(100,50,310,240)/N=FilterPanel as title
	ModifyPanel/W=FilterPanel, noEdit=1
		
	// insert a notebook subwindow to be used for filtering lists
	DefineGuide/W=FilterPanel nbR={FR,-28}
	NewNotebook/F=1/N=nb0/HOST=FilterPanel/W=(10,10,190,35)/FG=($"",$"",nbR,$"") /OPTS=3
	Notebook FilterPanel#nb0 fSize=12, showRuler=0
	Notebook FilterPanel#nb0 spacing={4,0,5}
	Notebook FilterPanel#nb0 margins={0,0,1000}
	SetWindow FilterPanel#nb0, activeChildFrame=0
	ClearText(1) // sets notebook to its default appearance
	
	// make a button for clearing text in notebook subwindow
	Button buttonClear, win=FilterPanel,pos={185,14},size={15,15},title=""
	Button buttonClear, Picture=ListFilter#ClearTextPicture,Proc=ListFilter#ButtonProc, disable=1
	ListBox listbox0, win=FilterPanel, pos={10,40}, size={190,120}, fsize=12, listwave=displayList
	ListBox listbox0, win=FilterPanel, mode=1, Proc=ListFilter#ListBoxProc, selRow=-1
	Button buttonDoIt, win=FilterPanel,pos={150,165},size={50,20},title="Do It"
	Button buttonDoIt, win=FilterPanel,Proc=ListFilter#ButtonProc, disable=2
	
	DoUpdate/W=FilterPanel
	
	// resizing userdata for controls
	Button buttonClear, win=FilterPanel,userdata(ResizeControlsInfo) = A"!!,GI!!#;m!!#<(!!#<(z!!#o2B4uAezzzzzzzzzzzzzz!!#o2B4uAezz"
	Button buttonClear, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#u:Du]k<zzzzzzzzzzz"
	Button buttonClear, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzz!!#u:Du]k<zzzzzzzzzzzzzz!!!"
	ListBox listbox0, win=FilterPanel,userdata(ResizeControlsInfo) = A"!!,A.!!#>.!!#AM!!#@Tz!!#](Aon\"Qzzzzzzzzzzzzzz!!#o2B4uAezz"
	ListBox listbox0, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#u:Du]k<zzzzzzzzzzz"
	ListBox listbox0, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzz!!#?(FEDG<zzzzzzzzzzzzzz!!!"
	Button buttonDoIt, win=FilterPanel,userdata(ResizeControlsInfo) = A"!!,G&!!#A4!!#>V!!#<Xz!!#o2B4uAezzzzzzzzzzzzzz!!#o2B4uAezz"
	Button buttonDoIt, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#?(FEDG<zzzzzzzzzzz"
	Button buttonDoIt, win=FilterPanel,userdata(ResizeControlsInfo) += A"zzz!!#?(FEDG<zzzzzzzzzzzzzz!!!"
	
	// resizing userdata for panel
	SetWindow FilterPanel,userdata(ResizeControlsInfo) = A"!!*'\"z!!#Aa!!#AMzzzzzzzzzzzzzzzzzzzzz"
	SetWindow FilterPanel,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzzzzzzzzzzzzzzz"
	SetWindow FilterPanel,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzzzzzzzzz!!!"
	SetWindow FilterPanel,userdata(ResizeControlsGuides) =  "nbR;"
	SetWindow FilterPanel,userdata(ResizeControlsInfonbR) =  "NAME:nbR;WIN:FilterPanel;TYPE:User;HORIZONTAL:0;POSITION:182.00;GUIDE1:FR;GUIDE2:;RELPOSITION:-28;"
	
	// resizing panel hook
	SetWindow FilterPanel hook(ResizeControls)=ResizeControls#ResizeControlsHook
	
	// filter hook
	SetWindow FilterPanel hook(hFilterHook)=ListFilter#FilterHook
end

static function ClearText(variable doIt)
	if (doIt)
		Notebook FilterPanel#nb0 selection={startOfFile,endofFile}, textRGB=(50000,50000,50000), text="Filter"
		Notebook FilterPanel#nb0 selection={startOfFile,startOfFile}
	endif
end

static function ButtonProc(STRUCT WMButtonAction &s)
	if (s.eventCode != 2)
		return 0
	endif
	strswitch(s.ctrlName)
		case "buttonClear":
			ClearText(1)
			SetWindow FilterPanel#nb0 userdata(stublen) = "0"
			Button buttonClear, win=FilterPanel, disable=3
			UpdateListboxWave("")
			break
		case "buttonDoIt":
			ControlInfo/W=FilterPanel listbox0
			if (V_Value >- 1)
				wave/T matchList = $(S_DataFolder+S_Value)
				doSomething(matchList[V_Value])
			else
				Button buttonDoIt, win=FilterPanel, disable=2
			endif
			break
	endswitch
	return 0
end

static function ListBoxProc(STRUCT WMListboxAction &s)
	switch (s.eventCode)
		case 2: // mouseup - this captures deselection by shift-click
			ControlInfo/W=$(s.win) $(s.ctrlName)
			s.row = V_value
		case 4:
		case 5:
			// a list item is selected
			Button buttonDoIt, win=FilterPanel, disable=2*(s.row>=DimSize(s.listwave,0) || s.row<0)
			break
		case 3: // double click
			if (s.row<DimSize(s.listwave,0) && s.row>=0)
				string selection = s.listwave[s.row]
				onDoubleClick(selection)
			endif
			break
	endswitch
	return 0
end

// update listbox wave based on string str
static function UpdateListboxWave(string str)
	DFREF dfr = root:Packages:ChooseFromListGUI
	wave/SDFR=dfr/T  displayList, fullList
	string regEx = "(?i)" + str
	Grep/Z/E=regEx fullList as displayList
	ListBox listbox0, win=FilterPanel, selRow=-1
	Button buttonDoIt, win=FilterPanel, disable=2
end

// intercept and deal with keyboard events in notebook subwindow
static function FilterHook(STRUCT WMWinHookStruct &s)
	if (s.eventcode == 2) // window is being killed
		KillDataFolder/Z root:Packages:ChooseFromListGUI
		return 1
	endif
	
	GetWindow/Z FilterPanel#nb0 active
	if (V_Value == 0) // this check is not reliable
		return 0
	endif
	
	if (s.eventCode==22  && cmpstr(s.winName, "FilterPanel#nb0")==0)
		return 1 // don't allow scrolling in notebook subwindow
	endif
	
	DFREF dfr = root:Packages:ChooseFromListGUI
	variable stubLen = str2num(GetUserData("FilterPanel#nb0", "", "stublen"))
	stubLen = numtype(stubLen) ? 0 : stubLen
	
	if (s.eventcode==3 && stubLen==0) // mousedown
		return 1
	endif
		
	if (s.eventcode == 5) // mouseup
		GetSelection Notebook, FilterPanel#nb0, 1 // get current position in notebook
		V_endPos = min(stubLen,V_endPos)
		V_startPos = min(stubLen,V_startPos)
		Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,V_endPos)}
		return 1
	endif
	
	if (s.eventcode == 10) // menu
		strswitch(s.menuItem)
			case "Paste":
				GetSelection Notebook, FilterPanel#nb0, 1 // get current position in notebook
				string strScrap = GetScrapText()
				strScrap = ReplaceString("\r", strScrap, "")
				strScrap = ReplaceString("\n", strScrap, "")
				strScrap = ReplaceString("\t", strScrap, "")
				Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,V_endPos)}, text=strScrap
				stubLen += strlen(strScrap)-abs(V_endPos-V_startPos)
				s.eventcode = 11
				// pretend this was a keyboard event to allow execution to continue
				break
			case "Cut":
				GetSelection Notebook, FilterPanel#nb0, 3 // get current position in notebook
				PutScrapText s_selection
				Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,V_endPos)}, text=""
				stubLen -= strlen(s_selection)
				s.eventcode = 11
				break
			case "Clear":
				GetSelection Notebook, FilterPanel#nb0, 3 // get current position in notebook
				Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,V_endPos)}, text="" // clear text
				stubLen -= strlen(s_selection)
				s.eventcode = 11
				break
		endswitch
		Button buttonClear, win=FilterPanel, disable=3*(stublen==0)
		ClearText((stubLen == 0))
	endif
				
	if (s.eventcode != 11)
		return 0
	endif
	
	if (stubLen == 0) // Remove "Filter" text before starting to deal with keyboard activity
		Notebook FilterPanel#nb0 selection={startOfFile,endofFile}, text=""
	endif
	
	// deal with some non-printing characters
	switch(s.keycode)
		case 9:	// tab: jump to end
		case 3:
		case 13: // enter or return: jump to end
			Notebook FilterPanel#nb0 selection={startOfFile,endofFile}, textRGB=(0,0,0)
			Notebook FilterPanel#nb0 selection={endOfFile,endofFile}
			GetSelection Notebook, FilterPanel#nb0, 1 // get current position in notebook
			stubLen = V_endPos
			break
		case 28: // left arrow
			ClearText(stubLen == 0); return 0
		case 29: // right arrow
			GetSelection Notebook, FilterPanel#nb0, 1
			if (V_endPos >= stubLen)
				if (s.eventMod & 2) // shift key
					Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,stubLen)}
				else
					Notebook FilterPanel#nb0 selection={(0,stubLen),(0,stubLen)}
				endif
				ClearText(stubLen==0); return 1
			endif
			ClearText(stubLen==0); return 0
		case 8:
		case 127: // delete or forward delete
			GetSelection Notebook, FilterPanel#nb0, 1
			if (V_startPos == V_endPos)
				V_startPos -= (s.keycode==8)
				V_endPos += (s.keycode==127)
			endif
			V_startPos = min(stubLen,V_startPos); V_endPos = min(stubLen,V_endPos)
			V_startPos = max(0, V_startPos); V_endPos = max(0, V_endPos)
			Notebook FilterPanel#nb0 selection={(0,V_startPos),(0,V_endPos)}, text=""
			stubLen -= abs(V_endPos-V_startPos)
			break
	endswitch
		
	// find and save current position
	GetSelection Notebook, FilterPanel#nb0, 1
	variable selEnd = V_endPos
		
	if (strlen(s.keyText) == 1) // a one-byte printing character
		// insert character into current selection
		Notebook FilterPanel#nb0 text=s.keyText, textRGB=(0,0,0)
		stubLen += 1 - abs(V_endPos-V_startPos)
		// find out where we want to leave cursor
		GetSelection Notebook, FilterPanel#nb0, 1
		selEnd = V_endPos
	endif
	
	string strStub = "", strInsert = "", strEnding = ""
		
	// select and format stub
	Notebook FilterPanel#nb0 selection={startOfFile,(0,stubLen)}, textRGB=(0,0,0)
	// get stub text
	GetSelection Notebook, FilterPanel#nb0, 3
	strStub = s_selection
	// get matches based on stub text
	UpdateListboxWave(strStub)
	
	// do auto-completion based on stubLen characters
	wave/T matchList = dfr:DisplayList
	
	if (s.keycode==30 || s.keycode==31) // up or down arrow
		Notebook FilterPanel#nb0 selection={(0,stubLen),endOfFile}
		GetSelection Notebook, FilterPanel#nb0, 3
		strEnding = s_selection
		strInsert = arrowKey(strStub, strEnding, 1-2*(s.keycode==30), matchList)
	else
		strInsert = completeStr(strStub, matchList)
	endif
	// insert completion text in grey
	Notebook FilterPanel#nb0 selection={(0,stubLen),endOfFile}, textRGB=(50000,50000,50000), text=strInsert
	Notebook FilterPanel#nb0 selection={startOfFile,startOfFile}, findText={"",1}
	Notebook FilterPanel#nb0 selection={(0,selEnd),(0,selEnd)}, findText={"",1}
	
	Button buttonClear, win=FilterPanel, disable=3*(stublen==0)
	ClearText(stubLen == 0)
	
	SetWindow FilterPanel#nb0 userdata(stublen) = num2str(stubLen)
	
	return 1 // tell Igor we've handled all keyboard events
end

// returns completion text for first match of string s in text wave w
static function/T completeStr(string stub, wave /T w)
	int stubLen = strlen(stub)
	if (stubLen == 0)
		return ""
	endif
	Make/free/T/N=1 w_out
	Grep/Z/E="(?i)^"+stub w as w_out
	if (DimSize(w_out,0) == 0)
		return ""
	endif
	return (w_out[0])[stubLen,Inf]
end

// find next or previous matching entry in wList and return completion text
static function/T arrowKey(string stub, string ending, variable increment, wave /T wList)
	int stubLen = strlen(stub)
	if (stubLen == 0)
		return ""
	endif
	Make/free/T/N=0 w_out
	Grep/Z/E="(?i)^"+stub wList as w_out
	if (numpnts(w_out) == 0)
		return ""
	endif
	FindValue/TEXT=stub+ending/TXOP=4/Z w_out
	if (v_value >- 1)
		v_value += increment
		v_value = V_value<0 ? numpnts(w_out)-1 : v_value
		v_value = V_value >= numpnts(w_out) ? 0 : v_value
	else
		return (w_out[0])[stubLen,Inf]
	endif
	return (w_out[v_value])[stubLen,Inf]
end

// PNG: width= 90, height= 30
static Picture ClearTextPicture
	ASCII85Begin
	M,6r;%14!\!!!!.8Ou6I!!!"&!!!!?#R18/!3BT8GQ7^D&TgHDFAm*iFE_/6AH5;7DfQssEc39jTBQ
	=U"$&q@5u_NKm@(_/W^%DU?TFO*%Pm('G1+?)0-OWfgsSqYDhC]>ST`Z)0"D)K8@Ncp@>C,GnA#([A
	Jb0q`hu`4_P;#bpi`?T]j@medQ0%eKjbh8pO.^'LcCD,L*6P)3#odh%+r"J\$n:)LVlTrTIOm/oL'r
	#E&ce=k6Fiu8aXm1/:;hm?p#L^qI6J;D8?ZBMB_D14&ANkg9GMLR*Xs"/?@4VWUdJ,1MBB0[bECn33
	KZ1__A<"/u9(o<Sf@<$^stNom5GmA@5SIJ$^\D=(p8G;&!HNh)6lgYLfW6>#jE3aT_'W?L>Xr73'A#
	m<7:2<I)2%%Jk$'i-7@>Ml+rPk4?-&B7ph6*MjH9&DV+Uo=D(4f6)f(Z9"SdCXSlj^V?0?8][X1#pG
	[0-Dbk^rVg[Rc^PBH/8U_8QFCWdi&3#DT?k^_gU>S_]F^9g'7.>5F'hcYV%X?$[g4KPRF0=NW^$Z(L
	G'1aoAKLpqS!ei0kB29iHZJgJ(_`LbUX%/C@J6!+"mVIs6V)A,gbdt$K*g_X+Um(2\?XI=m'*tR%i"
	,kQIh51]UETI+HBA)DV:t/sl4<N*#^^=N.<B%00/$P>lNVlic"'Jc$p^ou^SLA\BS?`$Jla/$38;!#
	Q+K;T6T_?\3*d?$+27Ri'PYY-u]-gEMR^<d.ElNUY$#A@tX-ULd\IU&bfX]^T)a;<u7HgR!i2]GBpt
	SiZ1;JHl$jf3!k*jJlX$(ZroR:&!&8D[<-`g,)N96+6gSFVi$$Gr%h:1ioG%bZgmgbcp;2_&rF/[l"
	Qr^V@O-"j&UsEk)HgI'9`W31Wh"3^O,KrI/W'chm_@T!!^1"Y*Hknod`FiW&N@PIluYQdKILa)RK=W
	Ub4(Y)ao_5hG\K-+^73&AnBNQ+'D,6!KY/`F@6)`,V<qS#*-t?,F98]@h"8Y7Kj.%``Q=h4.L(m=Nd
	,%6Vs`ptRkJNBdbpk]$\>hR4"[5SF8$^:q=W([+TB`,%?4h7'ET[Y6F!KJ3fH"9BpILuUI#GoI.rl(
	_DAn[OiS_GkcL7QT`\p%Sos;F.W#_'g]3!!!!j78?7R6=>B
	ASCII85End
end

 

ListFilter133.zip (4.63 KB)

Tony- I haven't tried your demo code yet; I'm sure it's terrific!

Are you aware of the ListBox keyword "keySelectCol"? A listbox control will move the selection to a row that matches what you have typed, as long as you type quickly enough. By default it looks at column zero, this keyword will change the column used for the purpose.

Yes, that functionality is not affected by adding a 'filtering' option.

The demo GUI is a bit like the command tab of the help browser, where you can either type-to-select or type in the filter area to refine the list.

Forum

Support

Gallery

Igor Pro 10

Learn More

Igor XOP Toolkit

Learn More

Igor NIDAQ Tools MX

Learn More