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 rtGlobals=3
#pragma IgorVersion=8
#pragma version=1.30
#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
    variable /G dfr:stubLen=0
       
    // 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)
            NVAR stubLen=root:Packages:ChooseFromListGUI:stubLen
            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
    NVAR stubLen=dfr: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={(0,selEnd),(0,selEnd)}, findText={"",1}
   
    Button buttonClear, win=FilterPanel, disable=3*(stublen==0)
    ClearText((stubLen==0))
    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

 

ListFilter130.zip

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 9

Learn More

Igor XOP Toolkit

Learn More

Igor NIDAQ Tools MX

Learn More