MultiUser Dialog Handler
Introduction
I found that i was constantly rewriting a pile of dialog handling functions in my scrips so i decided to write a single dialog handler script. It needed to have the following features;
- Support multiple simultaneous menus from different users.
- Support fixed top buttons that will persist on lists.
- Support fixed bottom buttons with include Back and Exit buttons.
- Handle item lists > 12 and automatically insert << and >> buttons to scroll through the lists.
- Handle timeouts and communicate timeout back to calling script.
- Handles truncation of the buttons and text to stop errors in llDialog
Using The Script
Calling a Simple Menu
In the script that is calling the menu I use a function something like this. This will display a menu that looks something like
This is the menu text that will be displayed.
Owner | Group | All |
Back | Exit |
<lsl> // Link Commands integer LINK_MENU_DISPLAY = 300; integer LINK_MENU_CLOSE = 310; integer LINK_MENU_RETURN = 320; integer LINK_MENU_TIMEOUT = 330;
DisplayMenu(key id)
{ // Menu Functions
string menuDescripter = "menudescriptor|TRUE"; //Menu name is 'menu with a parent menu name 'parent'. The display of the bottom navigation buttons is set to TRUE string menuText = "This is the menu text that will be displayed."; // The text ( will be trimmed to 510 characters string menuButtons = "Owner~Group~All"; // The buttons, each button separated by a '~'. Buttons will be trimmed to 24 characters llMessageLinked(LINK_THIS, LINK_MENU_DISPLAY, menuDescripter+"|"+menuText+"|"+ menuButtons, id);
} </lsl>
Calling a Complex Menu
The above case is a simple menu. The script can also do more complex menus.
In the following case we will have 10 buttons that are going to be displayed. Because we will also display the bottom 3 buttons this will be spanned across 2 menus. We will also include 2 fixed buttons that always display at the top of the menu, PLUS each button has a unique descriptor that is displayed.
This will display the following 2 menus
Menu1
Main text displayed. item1 item2 item3 item4 item5 item6 item7
FIXED1 | FIXED2 | B1 |
B2 | B3 | B4 |
B5 | B6 | B7 |
Back | >> |
Menu2 (on clicking the >> button)
Main text displayed. item8 item9 item10
FIXED1 | FIXED2 | B8 |
B9 | B10 | |
<< | Back | Exit |
<lsl> // Link Commands integer LINK_MENU_DISPLAY = 300; integer LINK_MENU_CLOSE = 310; integer LINK_MENU_RETURN = 320; integer LINK_MENU_TIMEOUT = 330;
DisplayMenu(key id)
{ // Menu Functions
string menuDescripter = "menudescriptor|TRUE"; string menuText = "Main text displayed.~text1~text2~text3~text4~text5~text6~text7~text8~text9~text10"; string menuButtons = "B1~B2~B3~B4~B5~B6~B7~B8~B9~B10"; string menuFixedButtons = "FIXED1~FIXED2" llMessageLinked(LINK_THIS, LINK_MENU_DISPLAY, menuDescripter+"|"+menuText+"|"+ menuButtons+"|"+menuFixedButtons, id);
} </lsl>
Parsing the returning Menu information
Note that if the user clicks on the << or >> buttons the calling script WILL NOT see any response. The correct menu is displayed. The calling script only receives a response when one of the buttons or Back/Exit are clicked. Clicking on a space button will cause the menu to redisplay.
The script that expects the response back from the menu has the following
<lsl> // Link Commands integer LINK_MENU_DISPLAY = 300; integer LINK_MENU_CLOSE = 310; integer LINK_MENU_RETURN = 320; integer LINK_MENU_TIMEOUT = 330;
default
{
link_message(integer intSenderNum, integer num, string message, key id) { if (num == LINK_MENU_RETURN) { list returnMenu = llParseString2List(message, ["|"], []);
string menuDescriptor = llList2String(returnMenu,0); // The idenfifying descriptor. I sometimes use the name of the script plus menu and parent menu name string item = llList2String(returnMenu,1); // The actual button pushed // id == the key of the menu user. // The button pressed in the menu is 'item', so now do something with it } else if (num == LINK_MENU_TIMEOUT) { // We received a timeout (don't worry the listens are all dealt with) so you might want to IM the menu user. } }
} </lsl>
The Script
<lsl> // ******************************************************************** // // Menu Display Script // // Menu command format // string = menuname ~ menuparent | display navigate? TRUE/FALSE | menuMaintitle~subtitle1~subtitle2~subtitle3 | button1~button2~button3 {| fixedbutton1~fixedbutton2~fixedbutton3 optional} // key = menuuser key // // // menusDescription [menuchannel, key, menu & parent, return link, nav?, titles, buttons, fixed buttons] // menusActive [menuchannel, menuhandle, time, page] // // by SimonT Quinnell // // ********************************************************************
// ********************************************************************
// CONSTANTS
// ********************************************************************
// Link Commands integer LINK_MENU_DISPLAY = 300; integer LINK_MENU_CLOSE = 310; integer LINK_MENU_RETURN = 320; integer LINK_MENU_TIMEOUT = 330;
// Main Menu Details string BACK = "<<"; string FOWARD = ">>"; list MENU_NAVIGATE_BUTTONS = [ " ", "Back", "Exit"]; float MENU_TIMEOUT_CHECK = 10.0; integer MENU_TIMEOUT = 120; integer MAX_TEXT = 510;
integer STRIDE_DESCRIPTION = 8;
integer STRIDE_ACTIVE = 4;
integer DEBUG = FALSE;
// ******************************************************************** // Variables // ********************************************************************
list menusDescription; list menusActive;
// ********************************************************************
// Functions - General
// ********************************************************************
debug(string debug) {
if (DEBUG) llWhisper(0,"DEBUG:"+llGetScriptName()+":"+debug+" : Free("+(string)llGetFreeMemory()+")");
}
integer string2Bool (string test)
{
if (test == "TRUE") return TRUE; else return FALSE;
}
// ******************************************************************** // Functions - Menu Helpers // ********************************************************************
integer NewChannel() { // generates unique channel number
integer channel;
do channel = -(llRound(llFrand(999999)) + 99999); while (~llListFindList(menusDescription, [channel]));
return channel;
}
string CheckTitleLength(string title)
{
if (llStringLength(title) > MAX_TEXT) title = llGetSubString(title, 0, MAX_TEXT-1); return title;
}
list FillMenu(list buttons)
{ //adds empty buttons until the list length is multiple of 3, to max of 12
integer i; list listButtons; for(i=0;i<llGetListLength(buttons);i++) { string name = llList2String(buttons,i); if (llStringLength(name) > 24) name = llGetSubString(name, 0, 23); listButtons = listButtons + [name]; } while (llGetListLength(listButtons) != 3 && llGetListLength(listButtons) != 6 && llGetListLength(listButtons) != 9 && llGetListLength(listButtons) < 12) { listButtons = listButtons + [" "]; } buttons = llList2List(listButtons, 9, 11); buttons = buttons + llList2List(listButtons, 6, 8); buttons = buttons + llList2List(listButtons, 3, 5); buttons = buttons + llList2List(listButtons, 0, 2); return buttons;
}
RemoveUser(key id) {
integer index_id = llListFindList(menusDescription, [id]); while (~index_id) { integer channel = llList2Integer(menusDescription, index_id-1); RemoveMenu(channel, FALSE); }
}
RemoveMenu(integer channel, integer echo) {
integer index = llListFindList(menusDescription, [channel]);
if (index != -1) { key id = llList2Key(menusDescription, index+1); menusDescription = llDeleteSubList(menusDescription, index, index + STRIDE_DESCRIPTION -1); RemoveListen(channel);
if (echo) llMessageLinked(LINK_THIS, LINK_MENU_TIMEOUT, "Timeout", id); }
}
RemoveListen(integer channel) {
integer index = llListFindList(menusActive, [channel]); if (index != -1) { llListenRemove(llList2Integer(menusActive, index + 1)); menusActive = llDeleteSubList(menusActive, index, index + STRIDE_ACTIVE - 1); }
}
// ******************************************************************** // Functions - Menu Main // ********************************************************************
NewMenu(string message, integer senderNum, key id) { // Setup New Menu
// menusDescription [menuchannel, key, menu & parent, return link, nav?, titles, buttons, fixed buttons]
list temp = llParseString2List(message, ["|"], []); integer channel = NewChannel(); if (llGetListLength(temp) > 2) { menusDescription = [channel, id, llList2String(temp, 0), senderNum, string2Bool(llList2String(temp, 1)), llList2String(temp, 2), llList2String(temp, 3), llList2String(temp, 4)] + menusDescription;
DisplayMenu(id, channel, 0); } else llOwnerSay ("ERROR: Dialog Script. Incorrect menu format");
}
DisplayMenu(key id, integer channel, integer page) {
string menuTitle; list menuSubTitles; list menuButtonsAll; list menuButtons; list menuNavigateButtons; list menuFixedButtons;
integer max = 12;
// Populate values integer index = llListFindList(menusDescription, [channel]); menuButtonsAll = llParseString2List(llList2String(menusDescription, index+6), ["~"], []); if (llList2String(menusDescription, index+7) != "") menuFixedButtons = llParseString2List(llList2String(menusDescription, index+7), ["~"], []);
// Set up the menu buttons if (llList2Integer(menusDescription, index+4)) menuNavigateButtons= MENU_NAVIGATE_BUTTONS; else if (llGetListLength(menuButtonsAll) > (max-llGetListLength(menuFixedButtons))) menuNavigateButtons = [" ", " ", " "]; // FIXME: add sanity check for menu page max = max - llGetListLength(menuFixedButtons) - llGetListLength(menuNavigateButtons); integer start = page*max; integer stop = (page+1)*max - 1; menuButtons = FillMenu(menuFixedButtons + llList2List(menuButtonsAll, start, stop));
// Generate the title list tempTitle = llParseString2List(llList2String(menusDescription, index+5), ["~"], []); menuTitle = llList2String(tempTitle,0); if (llGetListLength(tempTitle) > 1) menuSubTitles = llList2List(tempTitle, 1, -1); if (llGetListLength(menuSubTitles) > 0) { integer i; for(i=start;i<(stop+1);++i) { if (llList2String(menuSubTitles, i) != "") menuTitle += "\n"+llList2String(menuSubTitles, i); } } menuTitle = CheckTitleLength(menuTitle); // Add navigate buttons if necessary if (page > 0) menuNavigateButtons = llListReplaceList(menuNavigateButtons, [BACK], 0, 0); if (llGetListLength(menuButtonsAll) > (page+1)*max) menuNavigateButtons = llListReplaceList(menuNavigateButtons, [FOWARD], 2, 2); // Set up listen and add the row details integer menuHandle = llListen(channel, "", id, ""); menusActive = [channel, menuHandle, llGetUnixTime(), page] + menusActive;
llSetTimerEvent(MENU_TIMEOUT_CHECK);
// Display menu llDialog(id, menuTitle, menuNavigateButtons + menuButtons, channel);
}
// ********************************************************************
// Event Handlers
// ********************************************************************
default {
listen(integer channel, string name, key id, string message) { if (message == BACK) { integer index = llListFindList(menusActive, [channel]); integer page = llList2Integer(menusActive, index+3)-1; RemoveListen(channel); DisplayMenu(id, channel, page); } else if (message == FOWARD) { integer index = llListFindList(menusActive, [channel]); integer page = llList2Integer(menusActive, index+3)+1; RemoveListen(channel); DisplayMenu(id, channel, page); } else if (message == " ") { integer index = llListFindList(menusActive, [channel]); integer page = llList2Integer(menusActive, index+3); RemoveListen(channel); DisplayMenu(id, channel, page); } else { integer index = llListFindList(menusDescription, [channel]); llMessageLinked(llList2Integer(menusDescription, index+3), LINK_MENU_RETURN, llList2String(menusDescription, index+2)+"|"+message, id); RemoveMenu(channel, FALSE); } } link_message(integer senderNum, integer num, string message, key id) { if (num == LINK_MENU_DISPLAY) NewMenu(message, senderNum, id); else if (num == LINK_MENU_CLOSE) RemoveUser(id); } timer() { // Check through timers and close if necessary integer i; list toRemove; integer currentTime = llGetUnixTime(); integer length = llGetListLength(menusActive); for(i=0;i<length;i+=STRIDE_ACTIVE) { if (currentTime - llList2Integer(menusActive, i+2) > MENU_TIMEOUT) toRemove = [llList2Integer(menusActive, i)] + toRemove; }
length = llGetListLength(toRemove); if (length > 0) { for(i=0;i<length;i++) { RemoveMenu(llList2Integer(toRemove, i), TRUE); } } }
}
</lsl>