MultiUser Dialog Handler

From Second Life Wiki
Jump to navigation Jump to search

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, scripts and prims.
  • 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 = "mymenu";  //An identifier to show what menu called the script. I often include the script name plus menu name.
   string  menuNavigate = "TRUE";      //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+"|"+menuNavigate+"|"+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 = "mymenu"
   string  menuNavigate = "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+"|"+menuNavigate+"|"+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    
           
           if (menuDescriptor == "mymenu")
           {    // Is this my menu that is sending this message?
                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 = menuidentifier | display navigate? TRUE/FALSE | menuMaintitle~subtitle1~subtitle2~subtitle3 | button1~button2~button3 {| fixedbutton1~fixedbutton2~fixedbutton3 optional} // key = menuuser key // // Return is in the format // "menuidentifier | item" // // 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     menuId = llList2Key(menusDescription, index+1);
       string  menuDetails = llList2String(menusDescription, index+2);
       integer menuLink = llList2Integer(menusDescription, index+3);
       menusDescription = llDeleteSubList(menusDescription, index, index + STRIDE_DESCRIPTION - 1);
       RemoveListen(channel);
       if (echo) llMessageLinked(menuLink, LINK_MENU_TIMEOUT, menuDetails, menuId);
   }

}

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>

See also