#pragma TextEncoding = "UTF-8" #pragma rtGlobals=3 // Use modern global access method and strict wave access. #pragma ModuleName = Search //Using a module so I can have simple and generic function names that are globally accessible //Ben Murphy-Baum //March 2023 // Implements a live search control for user panels/windows //Run Search#ExampleLiveSearch() to run a quick implementation example, just make sure the current data folder has some waves in it for the tool to find // The control is implemented as a standard SetVariable control that takes a string input as a search term. // The control's functionality is adjusted so it updates the matched items in a drop down menu on every keystroke. // The effect is that the user gets a live menu update as they type. // The user can select a menu item with the mouse, or they can navigate using arrow keys and pressing return/enter. // For menus with lots of matched items, the scroll wheel allows users to scroll through items. // Users should define a trigger function that gets activated when the user makes a selection. // INPUT PARAMETERS: //****************** // win = windowStr : window that the control is in // pos = {left, top} // size = {width, height} // title = titleString : title of the SetVariable control // name = nameString : name of the SetVariable control // mode : 0 = uses searchWave to find items in a text wave // 1 = uses searchPath to find waves of any type // 2 = uses searchPath to find waves of 2D/3D dimensions // 3 = searches included user functions. // menurgba = {r,g,b,a} : background color of the menu // textrgb = {r,g,b} : text color of the menu for unselected items // selectrgb = {r,g,b} : text color of the menu item currently selected or hovered over // maxsize : maximum height of the menu when it expands for the search // fsize : font size of the title and menu items // font = fontString : font of the title and menu item // triggerFunction = functionString : name of the user-defined function that will be executed with a menu selection //******************* // Standard SetVariable parameters should be set using the SetVariable command: // win // pos // size // font // fsize // Custom parameters must be set using Search#Set(String windowNameStr, String controlNameStr, String parameterStr, String valueStr) // The set function inputs everything as a string, even if the original initialization used a variable or wave input. // e.g. For the example code in Search#ExampleLiveSearch(): // Search#Set("MyLiveSearchPanel","mySearchTool","maxsize","200") // would change the maximum height of the menu drop down to 200 points instead of maxing out at the bottom of the host window/panel // If users want to implement more search modes, they can add it to SearchFunction() with additional cases for the switch statement. // The function takes in a SearchInput, and that is used to match items in any way the user should desire. For example, you could set the // SearchFolder parameter (which is an input into SearchFunction()) to be anything (doesn't need to be a folder), and // use it to perform some arbitrary search routine. Static Function ExampleLiveSearch() KillWindow/Z MyLiveSearchPanel NewPanel/N=MyLiveSearchPanel/W=(50,50,350,600)/K=1 as "Live Search Example" DrawText/W=MyLiveSearchPanel 20,40,"Make sure there are waves in the current data \nfolder for the control to find!" LiveSearchControl(win="MyLiveSearchPanel",pos={50,75},size={200,20},title="Search Waves:",name="mySearchTool1",mode=1,maxsize=200,triggerFunction="Search#ExampleTrigger") LiveSearchControl(win="MyLiveSearchPanel",pos={50,300},size={200,20},title="Search Functions:",name="mySearchTool2",mode=3,maxsize=200,triggerFunction="Search#ExampleTrigger") End Static Function ExampleTrigger(String selectedItem) print "You selected: " + selectedItem End Function LiveSearchControl([win,pos,size,title,name,mode,searchPath,searchWave,menurgba,textrgb,selectrgb,maxsize,fsize,font,triggerFunction]) //Build the search tool String win Wave pos,size String title,name Variable mode String searchPath Wave/T/Z searchWave Wave menurgba,textrgb,selectrgb Variable maxsize,fsize String font String triggerFunction //Deal with defaults for building the control If(ParamIsDefault(win)) GetWindow kwTopWin activeSW win = S_Value EndIf If(!strlen(win)) return 0 EndIf If(ParamIsDefault(pos)) Variable left = 0 Variable top = 0 Else left = pos[0] top = pos[1] EndIf If(ParamIsDefault(size)) Variable width = 100 Variable height = 20 Else width = size[0] height = size[1] EndIf If(ParamIsDefault(title)) title = "SearchTool" EndIf If(ParamIsDefault(name)) name = UniqueName("SearchTool",15,0) EndIf If(ParamIsDefault(mode)) mode = 1 //default search for all waves in a folder EndIf If(ParamIsDefault(maxsize)) maxsize = -1 //no maximum, only limited by panel EndIf If(ParamIsDefault(fsize)) fsize = 12 EndIf If(ParamIsDefault(font)) font = GetDefaultFont(win) EndIf If(ParamIsDefault(searchPath)) searchPath = "" Else If(!DataFolderExists(searchPath)) searchPath = "" EndIf EndIf If(ParamIsDefault(searchWave)) Wave/T/Z searchWave = $"" Else If(!WaveExists(searchWave)) Wave/T/Z searchWave = $"" Else If(WaveType(searchWave,1) != 2) DoAlert/T="Live Search Tool" 0, "Search wave must be a text wave" Wave/T/Z searchWave = $"" EndIf EndIf EndIf If(ParamIsDefault(triggerFunction)) triggerFunction = "" EndIf If(ParamIsDefault(menurgba)) //Nice semi-transparent group box Variable r = 0x4000 Variable g = 0x4000 Variable b = 0x4000 Variable a = 0xffff * 0.15 Else r = menurgba[0] g = menurgba[1] b = menurgba[2] a = menurgba[3] EndIf //Search tool is based on SetVariable SetVariable/Z $name win=$win,pos={left,top},size={width,height},fsize=(fsize),title=title,font=font,value=_STR:"",proc=Search#LiveSearchToolProc //User data will hold our parameters for how and what gets searched SetVariable/Z $name userdata(triggerFunction) = triggerFunction SetVariable/Z $name userdata(group) = name + "_Group" SetVariable/Z $name userdata(searchPath) = searchPath SetVariable/Z $name userdata(mode) = num2str(mode) SetVariable/Z $name userdata(maxsize) = num2str(maxsize) SetVariable/Z $name userdata(menurgba) = num2str(r) + "," + num2str(g) + "," + num2str(b) + "," + num2str(a) //Text color If(ParamIsDefault(textrgb)) r = 0 g = 0 b = 0 Else r = textrgb[0] g = textrgb[1] b = textrgb[2] EndIf SetVariable/Z $name userdata(textrgb) = num2str(r) + "," + num2str(g) + "," + num2str(b) //Text selection color If(ParamIsDefault(selectrgb)) //Nice torqoise shade r = 0 g = 0xffff * 0.55 b = 0xffff * 0.75 Else r = selectrgb[0] g = selectrgb[1] b = selectrgb[2] EndIf SetVariable/Z $name userdata(selectrgb) = num2str(r) + "," + num2str(g) + "," + num2str(b) If(WaveExists(searchWave)) SetVariable/Z $name userdata(searchWave) = GetWavesDataFolder(searchWave,2) Else SetVariable/Z $name userdata(searchWave) = "" EndIf //Create these variables to hold data after the a given search tool is engaged. These get cleared after it disengages. DFREF DF = CreateFolder("root:Packages:CustomControls") Variable/G DF:MatchIndex = -1 String/G DF:SearchInput = "" String/G DF:SearchControl = "" String/G DF:SearchWindow = "" String/G DF:MatchList = "" String/G DF:SearchGroupBoxControl = "" String/G DF:SearchWave = "" String/G DF:SearchPath = "" String/G DF:TriggerFunctionStr = "" String/G DF:MenuColorStr = "0,0,0,0" String/G DF:TextColorStr = "0,0,0" String/G DF:SelectTextColorStr = "0,0,0" String/G DF:SearchMode = "0" String/G DF:MaxGroupSize = "-1" Variable/G DF:SearchOffset = 0 Variable/G DF:LastVisible = 0 End Static Function Set(String win, String ctrlName, String param, String value) //Sets the user data of an existing live search tool control ControlInfo/W=$win $ctrlName If(!V_flag) return 0 EndIf SetVariable $ctrlName win=$win, userdata($param) = value UpdateControlParameters() End Static Function/DF CreateFolder(path) //Creates a new data folder along the specified location //Creates the entire path if need be String path If(!strlen(path)) return $"" EndIf Variable i,numel = ItemsInList(path,":") For(i=1;i V_top + V_height || mouseTop < V_top || mouseLeft > V_left + V_width || mouseLeft < V_left ||V_height == 0) //Click off the menu MatchList = "" DrawAction/W=$SearchWindow/L=Overlay delete KillControl/W=$SearchWindow $SearchGroup String winStr = StringFromList(0,SearchWindow,"#") SetWindow $winStr,hook(LiveSearchToolHook)=$"" SetVariable/Z $SearchControl win=$SearchWindow,value=_STR:"",valueBackColor=0 break EndIf DrawAction/W=$SearchWindow/L=Overlay getgroup=SearchText,delete //Selection made, trigger function initiated String selection = StringFromList(MatchIndex,MatchList,";") DrawAction/W=$SearchWindow/L=Overlay getgroup=SearchText,delete Variable keepValue = TriggerFunction(selection) DrawAction/W=$SearchWindow/L=Overlay delete KillControl/W=$SearchWindow $SearchGroup winStr = StringFromList(0,SearchWindow,"#") SetWindow $winStr,hook(LiveSearchToolHook)=$"" If(keepValue) SetVariable $SearchControl win=$SearchWindow,valueBackColor=0 Else SetVariable $SearchControl win=$SearchWindow,value=_STR:"",valueBackColor=0 EndIf ClearSearchData() break case 4: //mouse moved GetMouse/W=$SearchWindow mouseLeft = V_left mouseTop = V_top SetActiveSubwindow $SearchWindow ControlInfo/W=$SearchWindow $SearchGroup If(mouseTop > V_top + V_height || mouseTop < V_top || mouseLeft > V_left + V_width || mouseLeft < V_left) break EndIf Variable delta = 20 //Determine which item has the mouse hovering over it Variable prevIndex = MatchIndex MatchIndex = SearchOffset + round((mouseTop-V_top + 5)/delta) - 1 If(prevIndex == MatchIndex) break EndIf //Display the function search menu DisplaySearchMenu(MatchList,delta,SearchOffset) break case 5: break case 11: //keyboard switch(s.specialKeyCode) case 0: //Standard keyboard input, text for the search bar SearchOffset = 0 //reset on more keystrokes MatchIndex = 0 ControlInfo/W=$SearchWindow $SearchControl SearchInput = S_Value + s.keyText SetVariable $SearchControl win=$SearchWindow,value=_STR:SearchInput //Use the input to create a contextual menu If(strlen(SearchInput)) //Find all the functions that match the text input MatchList = SearchFunction(SearchInput,SearchPath,SearchWave,SearchMode) //Display the function search menu DisplaySearchMenu(MatchList,20,SearchOffset) Else MatchList = "" DrawAction/W=$SearchWindow/L=Overlay delete KillControl/W=$SearchWindow searchGroup EndIf break case 300: //backspace/delete SearchOffset = 0 //reset on more keystrokes MatchIndex = 0 Variable shift = GetKeyState(0) If(shift && 4) ControlInfo/W=$SearchWindow $SearchControl SearchInput = "" MatchList = "" SetVariable $SearchControl win=$SearchWindow,value=_STR:SearchInput Else ControlInfo/W=$SearchWindow $SearchControl If(strlen(S_Value)) SearchInput = S_Value[0,strlen(S_Value)-2] SetVariable $SearchControl win=$SearchWindow,value=_STR:SearchInput EndIf EndIf //Use the input to create a fake contextual menu If(strlen(SearchInput)) //Find all the functions that match the text input MatchList = SearchFunction(SearchInput,SearchPath,SearchWave,SearchMode) //Display the function search menu DisplaySearchMenu(MatchList,20,SearchOffset) Else MatchList = "" DrawAction/W=$SearchWindow/L=Overlay delete KillControl/W=$SearchWindow $SearchGroup EndIf break case 200: case 201: //Enter/Return If(MatchIndex >= 0) //Selection triggered, go into the trigger function selection = StringFromList(MatchIndex,MatchList,";") DrawAction/W=$SearchWindow/L=Overlay delete keepValue = TriggerFunction(selection) EndIf DrawAction/W=$SearchWindow/L=Overlay delete KillControl/W=$SearchWindow $SearchGroup winStr = StringFromList(0,SearchWindow,"#") SetWindow $SearchWindow,hook(LiveSearchToolHook)=$"" If(keepValue) SetVariable $SearchControl win=$SearchWindow,valueBackColor=0 Else SetVariable $SearchControl win=$SearchWindow,value=_STR:"",valueBackColor=0 EndIf ClearSearchData() break case 103: //Down Arrow If(MatchIndex + 1 > ItemsInList(MatchList,";") - 1) MatchIndex = ItemsInList(MatchList,";") - 1 ElseIf(MatchIndex + 1 < 0) MatchIndex = 0 Else MatchIndex += 1 EndIf //Is the index the last visible item? If(MatchIndex > LastVisible) SearchOffset += 1 EndIf //Display the function search menu DisplaySearchMenu(MatchList,20,SearchOffset) break case 102: //Up Arrow If(MatchIndex - 1 < 0) MatchIndex = 0 ElseIf(MatchIndex - 1 > ItemsInList(MatchList,";") - 1) MatchIndex = ItemsInList(MatchList,";") - 1 Else MatchIndex -= 1 EndIf If(MatchIndex < SearchOffset) SearchOffset -= 1 EndIf //Display the function search menu DisplaySearchMenu(MatchList,20,SearchOffset) break endswitch break case 22: //Scroll wheel activated //What is my mouse's relative match index from the offset Variable relativeOffset = MatchIndex - SearchOffset SearchOffset -= s.wheelDy LastVisible -= s.wheelDy LastVisible = (SearchOffset < 0) ? LastVisible - s.wheelDy : LastVisible SearchOffset = (SearchOffset < 0) ? 0 : SearchOffset If(LastVisible > ItemsInList(MatchList,";") - 1) LastVisible += s.wheelDy SearchOffset += s.wheelDy EndIf //Holds the selected item where the mouse is MatchIndex = SearchOffset + relativeOffset //Display the function search menu DisplaySearchMenu(MatchList,20,SearchOffset) break endswitch return 0 End Static Function ClearSearchData() DFREF DF = root:Packages:CustomControls SVAR SearchWindow = DF:SearchWindow SVAR SearchControl = DF:SearchControl SVAR SearchInput = DF:SearchInput SVAR MatchList = DF:MatchList NVAR MatchIndex = DF:MatchIndex SVAR SearchGroup = DF:SearchGroupBoxControl SVAR SearchWave = DF:SearchWave SVAR SearchPath = DF:SearchPath SVAR SearchMode = DF:SearchMode NVAR SearchOffset = DF:SearchOffset NVAR LastVisible = DF:LastVisible SVAR MenuColorStr = DF:MenuColorStr SVAR TriggerFunctionStr = DF:TriggerFunctionStr SearchWindow = "" SearchControl = "" SearchInput = "" MatchList = "" MatchIndex = -1 //return to no selection SearchGroup = "" SearchWave = "" SearchPath = "" SearchMode = "" SearchOffset = 0 LastVisible = 0 MenuColorStr = "" TriggerFunctionStr = "" End Static Function DisplaySearchMenu(list,delta,offset) String list Variable delta Variable offset DFREF DF = root:Packages:CustomControls NVAR MatchIndex = DF:MatchIndex SVAR SearchWindow = DF:SearchWindow SVAR SearchControl = DF:SearchControl SVAR SearchGroup = DF:SearchGroupBoxControl SVAR MenuColorStr = DF:MenuColorStr SVAR TextColorStr = DF:TextColorStr SVAR SelectTextColorStr = DF:SelectTextColorStr SVAR MaxGroupSize = DF:MaxGroupSize NVAR LastVisible = DF:LastVisible NVAR SearchOffset = DF:SearchOffset STRUCT color menuColor String2Color(menuColor,MenuColorStr) STRUCT color textColor String2Color(textColor,TextColorStr) STRUCT color selectColor String2Color(selectColor,SelectTextColorStr) Variable pos ControlInfo/W=$SearchWindow $SearchControl Variable GroupBoxWidth = V_width Variable GroupBoxHeight = 5 pos = V_top + 5 Variable GroupBoxYPos = V_top + 20 Variable MenuTextXPos = V_left + 5 Variable ScrollBarPos = V_right - 10 GroupBox $(SearchControl + "_Group"),win=$SearchWindow,pos={V_left,GroupBoxYPos},size={GroupBoxWidth,GroupBoxHeight},labelBack=(menuColor.r,menuColor.g,menuColor.b,menuColor.a) SearchGroup = SearchControl + "_Group" //delete is slow. Need to find way to move the entire group up and down without deleting it entirely DrawAction/W=$SearchWindow/L=Overlay delete SetDrawLayer/W=$SearchWindow Overlay SetDrawEnv/W=$SearchWindow fsize=12,xcoord=abs,ycoord=abs,textyjust=2,gname=SearchText,gstart //Get the height of the window, so we know how far we can go down with the group box GetWindow/Z $SearchWindow wsize Variable winHeight = abs(V_bottom - V_top) Variable i,listSize = ItemsInList(list,";") If(SearchOffset > 0 || LastVisible < listSize - 1) Variable MakeScrollBar = 1 Else MakeScrollBar = 0 EndIf Variable maxSize = str2num(MaxGroupSize) maxsize = (maxsize == -1) ? inf : maxsize For(i=SearchOffset;i winHeight || GroupBoxHeight > maxsize) LastVisible = i - 1 GroupBoxHeight -=delta break EndIf GroupBox $SearchGroup,win=$SearchWindow,size={GroupBoxWidth,GroupBoxHeight} //Check for selection, indicate with colored text If(i == MatchIndex) SetDrawEnv/W=$SearchWindow fsize=13,xcoord=abs,ycoord=abs,textyjust=2,fstyle=0,textrgb=(selectColor.r,selectColor.g,selectColor.b),fname="Helvetica" Else SetDrawEnv/W=$SearchWindow fsize=12,xcoord=abs,ycoord=abs,textyjust=2,fstyle=0,textrgb=(textColor.r,textColor.g,textColor.b),fname="Helvetica Light" EndIf DrawText/W=$SearchWindow MenuTextXPos,pos,StringFromList(i,list,";") If(i == ItemsInList(list,";") - 1) //Did we reach the last matched item in the list? LastVisible = i If(!SearchOffset) MakeScrollBar = 0 EndIf EndIf EndFor SetDrawEnv/W=$SearchWindow gstop If(MakeScrollBar) Variable pctStart = SearchOffset / (listSize - 1) Variable pctEnd = LastVisible / (listSize - 1) SetDrawLayer/W=$SearchWindow Overlay SetDrawEnv/W=$SearchWindow gname = ScrollBarLine,gstart SetDrawEnv/W=$SearchWindow linethick=0,xcoord=abs,ycoord=abs,fillfgc=(0x8000,0x8000,0x8000,0xffff * 0.75) DrawRRect/W=$SearchWindow ScrollBarPos-3,pctStart * (GroupBoxHeight - 5) + (GroupBoxYPos + 5),ScrollBarPos+3,GroupBoxYPos + pctEnd * (GroupBoxHeight - 5) SetDrawEnv/W=$SearchWindow gstop EndIf End Function protoTriggerFunc(String selectedItem) print "You're using the prototype function. You should make your own triggerFunction for this control." print "BTW, you selected: " + selectedItem End Static Function/S SearchFunction(String SearchInput,String SearchFolder,String SearchWave, String SearchMode) String list = "" If(!strlen(SearchMode)) SearchMode = "1" EndIf strswitch(SearchMode) case "0": //Search a text wave for a value Wave/T/Z w = $SearchWave Variable i,j For(i=0;i