Difference between revisions of "SimpleDialogMenuSystem"

From Second Life Wiki
Jump to: navigation, search
(See Discussion Page for changes)
(Script rewritten as mentioned in the discussion page)
Line 1: Line 1:
 
{{LSL Header}} __NOTOC__
 
{{LSL Header}} __NOTOC__
<div id="box">
+
 
== Function: [[list]] giveDialog([[Key]] {{LSL Param|ID}}, [[integer]] {{LSL Param|pageNum}}); ==
+
 
<div style="padding: 0.5em;">
 
<div style="padding: 0.5em;">
Multi-paging, next and previous pages. There are fancier dialog menu routines; this is a bit more accessible, perhaps, to beginners.
+
A bare-bones multi-page dialog script, supporting next and previous page buttons.  
  
Even so, learners may find this script challenging enough. Tip! If it's just utility you are after -- getting a menu system in so you can get back to the rest of the script you are learning to write, then don't worry about how the main function, giveDialog(), actually works.
+
The variable 'gActionsPerPage' (initially set to 9) determines the number of useful buttons per page, apart from the page scroll buttons.
 +
9 is a good value. You can set it to any value between 1 and 12. If set to 11 or 12, the paging buttons will not appear, but no more than 11 or 12 choices may then exist in the action list.
  
Instead, ignore it, as it's fairly complex for a learner, and just focus on these things that you do need to change, and on getting them right, and then focus on processing the answers your menu gets from the user. The changes you need to make are relatively easy. And though the script looks long, the part you need to be in control of is actually very short!
+
'gListActions' is a very simplified example of menu choices available to the user. Replace the letters with action words appropriate to your application.
  
# Change any of the constant values at the top as per your needs at the time;
+
In various places, this code uses an integer where you might expect a float value, e.g. llSetTimerEvent(0).  This has been done in order to save space in the 'Byte Code' version of the script that exists once a script has been compiled. Often space is a more critical consideration than speed in script design.
#:For the list of DIALOG_CHOICES, you can hard code them in, as they are here, or as you get fancier, you can read them in -- from a notecard, from a list of inventory in the prim this script is in, from choices communicated to this script somehow, etc.
+
# If you are not offering a back button, follow directions in the code
+
#In the listen event:
+
#: Add some way to process the back buttton, if offering;
+
#: The final else in the listen event is an actual choice that made it through all the other evaluation. Do something with the choice.
+
  
See also: [[DialogMenus|Building a dialog menu step by step]]
+
The script uses state changes as a means of avoiding multi-user concurrency and multiple open listener issues. A state change ensures that any and all active listeners are released.
 +
The first detected touch sends the script into a 'busy' condition, and no more touches will be detected until that user has finished with the dialog(s), or the time for a response expires.
 +
Then the script returns to the 'ready' state, and a subsequent touch will be detected.
  
<lsl>
+
The 'ready' state uses touch_end() rather than touch_start() as this avoids the 'missed touch' problem, documented elsewhere, that occurs when changing state from within touch_start().
string msg = "Please make a choice.";
+
string msgTimeout = "Sorry. You snooze; you lose.";
+
float Timeout= 60.0;
+
list DIALOG_CHOICES = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","Y","Z"];
+
  
integer channel_dialog;
+
It would be simple to add floating text or a colour change to indicate when the script is in a 'ready' or 'busy' state.
integer listen_id;
+
key ToucherID;
+
  
//if not offering a back button, there are 3 things to change:
+
See also: [[DialogMenus|Building a dialog menu step by step]]
//MAX_DIALOG_CHOICES_PER_PG, and 2 code lines in the giveDialog function.
+
//It is noted in the function exactly where and how to change these.
+
  
integer N_DIALOG_CHOICES;
+
<lsl>
integer MAX_DIALOG_CHOICES_PER_PG = 8; // if not offering back button, increase this to 9
+
// Multi-Page Dialog Menu System
string PREV_PG_DIALOG_PREFIX = "< Page ";
+
// Omei Qunhua December 2013
string NEXT_PG_DIALOG_PREFIX = "> Page ";
+
string DIALOG_DONE_BTN = "Done";
+
integer   gActionsPerPage = 9;           // Number of action choice buttons per menu page (must be 1 to 10, or 12)
string DIALOG_BACK_BTN = "<< Back";
+
list      gListActions = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","Y","Z"];
integer pageNum;
+
 
+
// ========================================================================================================
giveDialog(key ID) {
+
    list buttons;
+
integer   gTotalActions;            
    integer firstChoice;
+
integer    gPage;                    // Current dialog page number (counting from zero)
    integer lastChoice;
+
integer    gMaxPage;                 // Highest page number (counting from zero)
     integer prevPage;
+
integer   gChan;                     // Channel used for dialog communications.
     integer nextPage;
+
key        gUser;                     // Current user accessing the dialogs
     string OnePage;
+
     N_DIALOG_CHOICES = llGetListLength(DIALOG_CHOICES);
+
BuildDialogPage(key user)
     if (N_DIALOG_CHOICES <= 10) {
+
{
        buttons = DIALOG_CHOICES;
+
     // Build a dialog menu for current page for given user
        OnePage = "Yes";
+
     integer start = gActionsPerPage * gPage;       // starting offset into action list for current page
     }
+
     // set up scrolling buttons as needed
     else {
+
     list buttons = [ "<<", " ", ">>" ];
        integer nPages = (N_DIALOG_CHOICES+MAX_DIALOG_CHOICES_PER_PG-1)/MAX_DIALOG_CHOICES_PER_PG;
+
     if (gActionsPerPage == 10)           buttons = [ "<<", ">>" ];
        if (pageNum < 1 || pageNum > nPages) {
+
    else if (gActionsPerPage > 10)      buttons = [];         // No room for paging buttons
            pageNum = 1;
+
        }
+
     // 'start + gActionsPerPage -1' might point beyond the end of the list -
        firstChoice = (pageNum-1)*MAX_DIALOG_CHOICES_PER_PG;
+
     // - but LSL stops at the list end, without throwing a wobbly
        lastChoice = firstChoice+MAX_DIALOG_CHOICES_PER_PG-1;
+
    buttons += llList2List(gListActions, start, start + gActionsPerPage - 1);
        if (lastChoice >= N_DIALOG_CHOICES) {
+
    llDialog(user, "\nPage " + (string) (gPage+1) + " of " + (string) (gMaxPage + 1) + "\n\nChoose an action", buttons, gChan);
            lastChoice = N_DIALOG_CHOICES;
+
    llSetTimerEvent(60);             // If no response in time, return to 'ready' state
        }
+
}
        if (pageNum <= 1) {
+
            prevPage = nPages;
+
default
            nextPage = 2;
+
{
         }
+
    state_entry()
         else if (pageNum >= nPages) {
+
    {
             prevPage = nPages-1;
+
        gTotalActions = (gListActions != [] );       // get length of action list
             nextPage = 1;
+
 +
         // Validate 'ActionsPerPage' value
 +
         if (gActionsPerPage < 1 || gActionsPerPage > 12)
 +
        {
 +
             llOwnerSay("Invalid 'gActionsPerPage' - must be 1 to 12");
 +
             return;
 
         }
 
         }
         else {
+
            prevPage = pageNum-1;
+
         // Compute number of menu pages that will be available
             nextPage = pageNum+1;
+
        gMaxPage = (gTotalActions - 1) / gActionsPerPage;
 +
        if (gActionsPerPage > 10)
 +
        {
 +
             gMaxPage = 0;
 +
            if (gTotalActions > gActionsPerPage)
 +
            {
 +
                llOwnerSay("Too many actions in total for this ActionsPerPage setting");
 +
                return;
 +
            }
 
         }
 
         }
         buttons = llList2List(DIALOG_CHOICES, firstChoice, lastChoice);
+
 +
         // Compute a negative communications channel based on prim UUID
 +
        gChan = 0x80000000 | (integer) ( "0x" + (string) llGetKey() );
 +
        state ready;
 
     }
 
     }
    // FYI, this puts the navigation button row first, so it is always at the bottom of the dialog
 
        list buttons01 = llList2List(buttons, 0, 2);
 
        list buttons02 = llList2List(buttons, 3, 5);
 
        list buttons03 = llList2List(buttons, 6, 8);
 
        list buttons04;
 
        if (OnePage == "Yes") {
 
            buttons04 = llList2List(buttons, 9, 11);
 
        }
 
        buttons = buttons04 + buttons03 + buttons02 + buttons01;
 
        if (OnePage == "Yes") {
 
            buttons = [ DIALOG_DONE_BTN, DIALOG_BACK_BTN ]+ buttons;
 
            //omit DIALOG_BACK_BTN in line above  if not offering
 
           
 
        }
 
        else {
 
            buttons =
 
            (buttons=[])+
 
            [ PREV_PG_DIALOG_PREFIX+(string)prevPage,
 
            DIALOG_BACK_BTN, NEXT_PG_DIALOG_PREFIX+(string)nextPage, DIALOG_DONE_BTN
 
            ]+buttons;
 
          //omit DIALOG_BACK_BTN in line above if not offering
 
        }
 
        llSetTimerEvent(Timeout);
 
        llDialog(ID, "Page "+(string)pageNum+"\n" + msg, buttons, channel_dialog);
 
 
}
 
}
 
+
state ready
 
+
{
CancelListen() {
+
     touch_end(integer total_number)
     llListenRemove(listen_id);
+
     {
     llSetTimerEvent(0.0);
+
        gUser = llDetectedKey(0);
 +
        state busy;                                   
 +
        // Changing state sets the application to a busy condition while one user is selecting from the dialogs
 +
        // In the event of multiple 'simultaneous' touches, only one user will get a dialog
 +
    }
 
}
 
}
 
+
state busy
default{
+
{
     state_entry() {
+
     state_entry()
         channel_dialog = ( -1 * (integer)("0x"+llGetSubString((string)llGetKey(),-5,-1)) );
+
    {
 +
         llListen(gChan, "", gUser, "");                // This listener will be used throughout this state
 +
        gPage = 0;
 +
        BuildDialogPage(gUser);                       // Show  Page 0 dialog to current user
 
     }
 
     }
 
+
     listen (integer chan, string name, key id, string msg)
    touch_start(integer total_number) {
+
    {
        ToucherID = llDetectedKey(0);
+
         if (msg == "<<" || msg == ">>")                  // Page change ...
        listen_id = llListen( channel_dialog, "", ToucherID, "");
+
         {
        pageNum = 1;
+
            if (msg == "<<")        --gPage;              // Page back
        giveDialog(ToucherID, pageNum);
+
            if (msg == ">>")       ++gPage;              // Page forward
    }
+
             if (gPage < 0)         gPage = gMaxPage;     // cycle around pages
 
+
            if (gPage > gMaxPage)  gPage = 0;
 
+
             BuildDialogPage(id);
     listen(integer channel, string name, key id, string choice) {
+
         //here, you need to only:
+
        //1. implement something that happens when the back button is pressed, or omit back button
+
        //2. Go to the else event. That is where any actual choice is. Process that choice.
+
         if (choice == "-") {
+
             giveDialog(ToucherID);
+
        }
+
        else if ( choice == DIALOG_DONE_BTN){
+
             CancelListen();
+
 
             return;
 
             return;
 
         }
 
         }
         else if (choice == DIALOG_BACK_BTN) {
+
         if (msg != " ")                                 // no action on blank menu button
            CancelListen();
+
         {
            //go back to where you want
+
             // User has selected an action from the menu
         }
+
             llRegionSayTo(id, 0, "You chose action <" + msg + ">");
        else if (llSubStringIndex(choice, PREV_PG_DIALOG_PREFIX) == 0) {
+
             pageNum = (integer)llGetSubString(choice, llStringLength(PREV_PG_DIALOG_PREFIX), -1);
+
            giveDialog(ToucherID);
+
        }
+
        else if (llSubStringIndex(choice, NEXT_PG_DIALOG_PREFIX) == 0) {
+
            pageNum = (integer)llGetSubString(choice, llStringLength(NEXT_PG_DIALOG_PREFIX), -1);
+
            giveDialog(ToucherID);
+
        }
+
        else { //this is the section where you do stuff
+
             llSay(0, "You chose " + choice);
+
            //okay anything else left is an actual choice, so do something with it
+
 
         }
 
         }
 +
        state ready;        // changing state will release ANY and ALL open listeners
 
     }
 
     }
 
+
     timer()
     timer() {
+
    {
         CancelListen();
+
         llRegionSayTo(gUser, 0, "Too slow, menu cancelled");
         llWhisper(0, msgTimeout);
+
         state ready;
 +
    }
 +
    state_exit()
 +
    {
 +
        llSetTimerEvent(0);         // would be dangerous to leave a dormant timer
 
     }
 
     }
 
}
 
}

Revision as of 10:15, 13 December 2013

A bare-bones multi-page dialog script, supporting next and previous page buttons.

The variable 'gActionsPerPage' (initially set to 9) determines the number of useful buttons per page, apart from the page scroll buttons. 9 is a good value. You can set it to any value between 1 and 12. If set to 11 or 12, the paging buttons will not appear, but no more than 11 or 12 choices may then exist in the action list.

'gListActions' is a very simplified example of menu choices available to the user. Replace the letters with action words appropriate to your application.

In various places, this code uses an integer where you might expect a float value, e.g. llSetTimerEvent(0). This has been done in order to save space in the 'Byte Code' version of the script that exists once a script has been compiled. Often space is a more critical consideration than speed in script design.

The script uses state changes as a means of avoiding multi-user concurrency and multiple open listener issues. A state change ensures that any and all active listeners are released. The first detected touch sends the script into a 'busy' condition, and no more touches will be detected until that user has finished with the dialog(s), or the time for a response expires. Then the script returns to the 'ready' state, and a subsequent touch will be detected.

The 'ready' state uses touch_end() rather than touch_start() as this avoids the 'missed touch' problem, documented elsewhere, that occurs when changing state from within touch_start().

It would be simple to add floating text or a colour change to indicate when the script is in a 'ready' or 'busy' state.

See also: Building a dialog menu step by step

<lsl> // Multi-Page Dialog Menu System // Omei Qunhua December 2013

integer gActionsPerPage = 9; // Number of action choice buttons per menu page (must be 1 to 10, or 12) list gListActions = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","Y","Z"];

// ========================================================================================================

integer gTotalActions; integer gPage; // Current dialog page number (counting from zero) integer gMaxPage; // Highest page number (counting from zero) integer gChan; // Channel used for dialog communications. key gUser; // Current user accessing the dialogs

BuildDialogPage(key user) {

   // Build a dialog menu for current page for given user
   integer start = gActionsPerPage * gPage;       // starting offset into action list for current page
   // set up scrolling buttons as needed
   list buttons = [ "<<", " ", ">>" ];
   if (gActionsPerPage == 10)           buttons = [ "<<", ">>" ];
   else if (gActionsPerPage > 10)       buttons = [];          // No room for paging buttons

   // 'start + gActionsPerPage -1' might point beyond the end of the list -
   // - but LSL stops at the list end, without throwing a wobbly
   buttons += llList2List(gListActions, start, start + gActionsPerPage - 1);
   llDialog(user, "\nPage " + (string) (gPage+1) + " of " + (string) (gMaxPage + 1) + "\n\nChoose an action", buttons, gChan);
   llSetTimerEvent(60);              // If no response in time, return to 'ready' state

}

default {

   state_entry()
   {
       gTotalActions = (gListActions != [] );        // get length of action list

       // Validate 'ActionsPerPage' value
       if (gActionsPerPage < 1 || gActionsPerPage > 12)
       {
           llOwnerSay("Invalid 'gActionsPerPage' - must be 1 to 12");
           return;
       }

       // Compute number of menu pages that will be available
       gMaxPage = (gTotalActions - 1) / gActionsPerPage;
       if (gActionsPerPage > 10)
       {
           gMaxPage = 0;
           if (gTotalActions > gActionsPerPage)
           {
               llOwnerSay("Too many actions in total for this ActionsPerPage setting");
               return;
           }
       }

       // Compute a negative communications channel based on prim UUID
       gChan = 0x80000000 | (integer) ( "0x" + (string) llGetKey() );
       state ready;
   }

} state ready {

   touch_end(integer total_number)
   {
       gUser = llDetectedKey(0);
       state busy;                                    
       // Changing state sets the application to a busy condition while one user is selecting from the dialogs
       // In the event of multiple 'simultaneous' touches, only one user will get a dialog
   }

} state busy {

   state_entry()
   {
       llListen(gChan, "", gUser, "");                // This listener will be used throughout this state
       gPage = 0;
       BuildDialogPage(gUser);                        // Show  Page 0 dialog to current user
   }
   listen (integer chan, string name, key id, string msg)
   {
       if (msg == "<<" || msg == ">>")                   // Page change ...
       {
           if (msg == "<<")        --gPage;              // Page back
           if (msg == ">>")        ++gPage;              // Page forward
           if (gPage < 0)          gPage = gMaxPage;     // cycle around pages
           if (gPage > gMaxPage)   gPage = 0;
           BuildDialogPage(id);
           return;
       }
       if (msg != " ")                                  // no action on blank menu button
       {
           // User has selected an action from the menu
           llRegionSayTo(id, 0, "You chose action <" + msg + ">");
       }
       state ready;         // changing state will release ANY and ALL open listeners
   }
   timer()
   {
       llRegionSayTo(gUser, 0, "Too slow, menu cancelled");
       state ready;
   }
   state_exit()
   {
       llSetTimerEvent(0);          // would be dangerous to leave a dormant timer
   }

} </lsl>