Difference between revisions of "MultiUser Dialog Handler"

From Second Life Wiki
Jump to navigation Jump to search
m (<lsl> tag to <source>)
 
(32 intermediate revisions by one other user not shown)
Line 3: Line 3:
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;
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 multiple simultaneous menus from different users, scripts and prims.
* Support fixed top buttons that will persist on lists.
* Support fixed top buttons that will persist on lists.
* Support fixed bottom buttons with include Back and Exit buttons.
* Support fixed bottom buttons with include Back and Exit buttons.
Line 13: Line 13:


===Calling a Simple Menu===
===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
In the script that is calling the menu I use a function something like this.
 
<source lang="lsl2">
// Link Commands
integer    LINK_MENU_DISPLAY = 300;
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 = "FALSE";      //Navigation buttons not displayed
    string  menuText = "This is the menu text that will be displayed.";  // The text will be trimmed to 510 characters
    string  menuButtons =  "TopLeft~Top~TopRight~Left~Center~Right~BottomLeft~Bottom~BottomRight";  // The buttons, each button separated by a '~'.
       
    llMessageLinked(LINK_THIS, LINK_MENU_DISPLAY, menuDescripter+"|"+menuNavigate+"|"+menuText+"|"+ menuButtons, id);
}
</source>
 
This will display a menu that looks something like


  This is the menu text that will be displayed.
  This is the menu text that will be displayed.
   
   
{| border="1"
{| border="1"
|Owner || Group || All
|TopLeft|| Top|| TopRight
|-
|Left|| Center || Right
|-
|-
|     || Back  || Exit
|BottomLeft|| Bottom || BottomRight
|}
|}


===Calling a Simple Menu with Auto-Navigation Buttons===
In this case we now add navigation buttons to the bottom of the script.  Back and Exit buttons are added to the bottom and if necessary >> and << buttons.  In the script that is calling the menu I use a function something like this. 


 
<source lang="lsl2">
<lsl>
// Link Commands
// Link Commands
integer    LINK_MENU_DISPLAY = 300;
integer    LINK_MENU_DISPLAY = 300;
integer    LINK_MENU_CLOSE = 310;
integer    LINK_MENU_RETURN = 320;
integer    LINK_MENU_RETURN = 320;
integer    LINK_MENU_TIMEOUT = 330;
integer    LINK_MENU_TIMEOUT = 330;
Line 35: Line 57:
DisplayMenu(key id)
DisplayMenu(key id)
{  // Menu Functions
{  // Menu Functions
     string  menuDescripter = "menudescriptor|TRUE";  //An identifier to show what menu called the script.  The display of the bottom navigation buttons is set to TRUE
     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  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
     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);
     llMessageLinked(LINK_THIS, LINK_MENU_DISPLAY, menuDescripter+"|"+menuNavigate+"|"+menuText+"|"+ menuButtons, id);
}
}
</lsl>
</source>
 
 
This will display a menu that looks something like this.
 
This is the menu text that will be displayed.
{| border="1"
|Owner || Group || All
|-
|      || Back  || Exit
|}


===Calling a Complex Menu===
===Calling a Complex Menu===
The above case is a simple menuThe script can also do more complex menus.   
In the following case we will have 10 buttons that are going to be displayedBecause 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.  Here is the menu call.


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.
<source lang="lsl2">
// Link Commands
integer    LINK_MENU_DISPLAY = 300;
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);
}
</source>


This will display the following 2 menus
This will display the following 2 menus
Line 83: Line 134:
| <<    ||  Back  || Exit
| <<    ||  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 = "mymenuext 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===
===Parsing the returning Menu information===
Line 108: Line 141:
The script that expects the response back from the menu has the following
The script that expects the response back from the menu has the following


<lsl>
<source lang="lsl2">
// Link Commands
// Link Commands
integer    LINK_MENU_DISPLAY = 300;
integer    LINK_MENU_DISPLAY = 300;
integer    LINK_MENU_CLOSE = 310;
integer    LINK_MENU_RETURN = 320;
integer    LINK_MENU_RETURN = 320;
integer    LINK_MENU_TIMEOUT = 330;
integer    LINK_MENU_TIMEOUT = 330;
Line 126: Line 158:
              
              
             if (menuDescriptor == "mymenu")
             if (menuDescriptor == "mymenu")
             {    // Is this my menu that is sending this message?
             {    // Is this a response to "mymenu" from the dialog script?
                 string  item = llList2String(returnMenu,1);            // The actual button pushed
                 string  item = llList2String(returnMenu,1);            // The actual button pushed


Line 135: Line 167:
         else if (num == LINK_MENU_TIMEOUT)
         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.
             if (message == "mymenu")
            {
                // We received a timeout (don't worry the listens are all dealt with) so you might want to IM the menu user.
            }
         }
         }
     }
     }
}
}
</lsl>
</source>


==The Script==
==The Script==
<lsl>
<source lang="lsl2">
// ********************************************************************
// ********************************************************************
//
//
Line 148: Line 183:
//
//
// Menu command format
// Menu command format
// string = menuname ~ menuparent | display navigate? TRUE/FALSE | menuMaintitle~subtitle1~subtitle2~subtitle3 | button1~button2~button3 {| fixedbutton1~fixedbutton2~fixedbutton3  optional}
// string = menuidentifier | display navigate? TRUE/FALSE | menuMaintitle~subtitle1~subtitle2~subtitle3 | button1~button2~button3 {| fixedbutton1~fixedbutton2~fixedbutton3  optional}
// key = menuuser key
// key = menuuser key
//
//
// Return is in the format
// "menuidentifier | item"
//
//
// menusDescription [menuchannel, key, menu & parent, return link, nav?,  titles, buttons, fixed buttons]
// menusDescription [menuchannel, key, menu & parent, return link, nav?,  titles, buttons, fixed buttons]
// menusActive      [menuchannel, menuhandle, time, page]  
// menusActive      [menuchannel, menuhandle, time, page]  
//
//
// by SimonT Quinnell  
// by SimonT Quinnell
//
// CHANGES
// 2010/10/14 - Timeout message now gets sent to the prim that called the menu, not LINK_THIS.  Also includes menuidentifier
// 2010/11/29 - Fixed Bug in RemoveUser function.  Thanks for Virtouse Lilienthal for pointing it out.
// 2010/11/29 - Tidied up a little and removed functions NewMenu and RemoveUser that are only called once
// 2014/04/28 - Clarified licence
//
// NOTE: This script is licenced using the Creative Commons Attribution-Share Alike 3.0 license
//
//
// ********************************************************************
// ********************************************************************
Line 197: Line 242:
debug(string debug)
debug(string debug)
{
{
     if (DEBUG) llWhisper(0,"DEBUG:"+llGetScriptName()+":"+debug+" : Free("+(string)llGetFreeMemory()+")");
     if (DEBUG) llSay(DEBUG_CHANNEL,"DEBUG:"+llGetScriptName()+":"+debug+" : Free("+(string)llGetFreeMemory()+")");
}   
}   
                  
                  
Line 253: Line 298:
      
      
     return buttons;
     return buttons;
}
RemoveUser(key id)
{
    integer index_id = llListFindList(menusDescription, [id]);
   
    while (~index_id)
    {
        integer channel = llList2Integer(menusDescription, index_id-1);
        RemoveMenu(channel, FALSE);
    }
}
}


Line 272: Line 306:
     if (index != -1)
     if (index != -1)
     {
     {
         key id = llList2Key(menusDescription, index+1);
         key     menuId = llList2Key(menusDescription, index+1);
         menusDescription = llDeleteSubList(menusDescription, index, index + STRIDE_DESCRIPTION -1);
        string  menuDetails = llList2String(menusDescription, index+2);
        integer menuLink = llList2Integer(menusDescription, index+3);
         menusDescription = llDeleteSubList(menusDescription, index, index + STRIDE_DESCRIPTION - 1);
         RemoveListen(channel);
         RemoveListen(channel);


         if (echo) llMessageLinked(LINK_THIS, LINK_MENU_TIMEOUT, "Timeout", id);
         if (echo) llMessageLinked(menuLink, LINK_MENU_TIMEOUT, menuDetails, menuId);
     }
     }
}
}
Line 293: Line 329:
// Functions - Menu Main
// 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)
DisplayMenu(key id, integer channel, integer page)
Line 405: Line 425:
     link_message(integer senderNum, integer num, string message, key id)  
     link_message(integer senderNum, integer num, string message, key id)  
     {
     {
         if (num == LINK_MENU_DISPLAY) NewMenu(message, senderNum, id);
         if (num == LINK_MENU_DISPLAY)
         else if (num == LINK_MENU_CLOSE) RemoveUser(id);
        {  // Setup New Menu
            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 llSay (DEBUG_CHANNEL, "ERROR in "+llGetScriptName()+": Dialog Script. Incorrect menu format");
        }
         else if (num == LINK_MENU_CLOSE)
        {    // Will remove all menus that have the user id.
            integer index_id = llListFindList(menusDescription, [id]);
   
            while (~index_id)
            {
                integer channel = llList2Integer(menusDescription, index_id-1);
                RemoveMenu(channel, FALSE);
       
                // Check for another menu by same user
                index_id = llListFindList(menusDescription, [id]);
            }
        }
     }
     }
      
      
Line 432: Line 476:
}
}


</lsl>
</source>


==See also==
==See also==
*[[ Script_Library | Script Library ]]
*[[ Script_Library | Script Library ]]
[[Category:LSL Library|Dialog Handler (MultiUser)]]

Latest revision as of 19:57, 24 January 2015

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.

// Link Commands
integer     LINK_MENU_DISPLAY = 300;
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 = "FALSE";      //Navigation buttons not displayed
    string  menuText = "This is the menu text that will be displayed.";  // The text will be trimmed to 510 characters
    string  menuButtons =  "TopLeft~Top~TopRight~Left~Center~Right~BottomLeft~Bottom~BottomRight";   // The buttons, each button separated by a '~'.
        
    llMessageLinked(LINK_THIS, LINK_MENU_DISPLAY, menuDescripter+"|"+menuNavigate+"|"+menuText+"|"+ menuButtons, id);
}

This will display a menu that looks something like

This is the menu text that will be displayed.

TopLeft Top TopRight
Left Center Right
BottomLeft Bottom BottomRight

Calling a Simple Menu with Auto-Navigation Buttons

In this case we now add navigation buttons to the bottom of the script. Back and Exit buttons are added to the bottom and if necessary >> and << buttons. In the script that is calling the menu I use a function something like this.

// Link Commands
integer     LINK_MENU_DISPLAY = 300;
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);
}


This will display a menu that looks something like this.

This is the menu text that will be displayed.

Owner Group All
Back Exit

Calling a Complex Menu

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. Here is the menu call.

// Link Commands
integer     LINK_MENU_DISPLAY = 300;
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);
}

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

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

// Link Commands
integer     LINK_MENU_DISPLAY = 300;
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 a response to "mymenu" from the dialog script?
                 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)
         {
             if (message == "mymenu")
             {
                // We received a timeout (don't worry the listens are all dealt with) so you might want to IM the menu user.
             }
         }
    }
}

The Script

// ********************************************************************
//
// 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
//
// CHANGES
// 2010/10/14 - Timeout message now gets sent to the prim that called the menu, not LINK_THIS.  Also includes menuidentifier
// 2010/11/29 - Fixed Bug in RemoveUser function.  Thanks for Virtouse Lilienthal for pointing it out.
// 2010/11/29 - Tidied up a little and removed functions NewMenu and RemoveUser that are only called once
// 2014/04/28 - Clarified licence
//
// NOTE: This script is licenced using the Creative Commons Attribution-Share Alike 3.0 license
//
// ********************************************************************


// ********************************************************************
// 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) llSay(DEBUG_CHANNEL,"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;
}

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
// ********************************************************************

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)
        {   // Setup New Menu
            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 llSay (DEBUG_CHANNEL, "ERROR in "+llGetScriptName()+": Dialog Script. Incorrect menu format");
        }
        else if (num == LINK_MENU_CLOSE)
        {    // Will remove all menus that have the user id.
             integer index_id = llListFindList(menusDescription, [id]);
    
             while (~index_id) 
             {
                 integer channel = llList2Integer(menusDescription, index_id-1);
                 RemoveMenu(channel, FALSE);
        
                 // Check for another menu by same user
                 index_id = llListFindList(menusDescription, [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);
            }
        }        
    }
}

See also