Description
Menu on a Prim (hereupon called the "menu system") is a subsystem allowing application scripts to display interactive menus (somewhat like the native LSL llDialog() feature) using OpenSim's prim drawing capabilities.
It has the following advantages over the native method:
There is no "Ignore" button, which by its undetectability often introduces complications to the UI
It can be seen by anyone in-world, and potentially used collaboratively
Easier for applications to use (no listener, no timeout event handling, button lists intuitively sequenced, etc)
Buttons can be wider, can be different colours (for clarity), can be left-aligned, automatically "smart sorted", etc
No limit of 12 buttons
Menu does not have to be redisplayed if still active after option selection
Open to future additional enhancements
Cleaner and more modern looking
The aesthetics of the menu are very loosely based on the Android "Material Design" guidelines, giving a clean appearance with a white background.
Construction
The menu prim should be a flattened box forming a rectangle whose height and width follow the rules set by the OSSL prim drawing features; these appear not to be documented anywhere, but experimentation suggest that the aspect ratio should be 1:<n> (or <n>:1) where <n> is an exponent of 2. So 1:1, 2:1, 4:1, etc are all valid aspect ratios.
The menu script resides inside the menu prim.
In addition, a prim with the name "MenuMask" will be treated as a mask that covers the menu while it is being drawn, to avoid the grey texture and blurriness seen while the prim drawing takes place. This prim should be the same size as the menu prim, but slightly thicker such that it obscures the menu prim. While the menu system will work without the mask, it does improve the appearance of the menu in action. The mask may be textured with the "Loading menu ..." texture for consistency in font and appearance.
The menu script will use prim slicing to hide the menu prim and mask prim when not in use - the menu sliced to (0, 0.03) and the mask to (0, 02). This makes it possible to select the menu prim while sliced since it is slightly taller.
At the moment, no testing has been done with multiple menu prims, and the naming convention for the mask means that only one mask prim is possible in a linkset.
Application interface
This section lays out how the application can control the menus and respond to them. Examples are used for illustration.
1. Basics
The menu system interface uses linked messages (LMs) to send and receive data with the application. As usual, unique values in the integer portion of the LM specify the type of data being sent, and the string (and possibly key) portions contain the data itself.
The following function could be used to wrap llMessageLinked() in order to send messages:
Menu(integer Command, string Text) {
llMessageLinked(LINK_SET, Command, Text, NULL_KEY);
}
This could be refined by sending the LMs directly to the menu prim, but this is not necessary since the LM numbers are effectively unique to the menu system by virtue of obscurity.
In addition, we can declare the full range of integer pseudo-constants used by the menu system:
integer MENU_INIT = -30151400;
integer MENU_TITLE = -30151401;
integer MENU_DESCRIPTION = -30151402;
integer MENU_ACTIVATE = -30151404;
integer MENU_DIMENSIONS = -30151405;
integer MENU_CLEAR = -30151406;
integer MENU_OPTION = -30151407;
integer MENU_SORT = -30151408;
integer MENU_BUTTON = -30151409;
integer MENU_USER = -30151410;
integer MENU_RESPONSE = -30151411;
integer MENU_RESET = -30151412;
integer MENU_SIDES = -30151413;
integer MENU_CANCEL = -30151414;
So, to send the menu system the title, we can use:
Menu(MENU_TITLE, "Our Title");
which is functionally the same as:
llMessageLinked(LINK_SET, -30151401, "Our Title", NULL_KEY);
but, of course, much easier to code, as well as to read and understand.
2. Initialising the menus
Certain functions need to be done to set up the menu's general parameters before any buttons are added. We'll go through the example line-by-line:
Menu(MENU_INIT, "");
This initialises the menus, ensuring that we're starting from a clean slate.
Menu(MENU_DIMENSIONS, "512, 512");
Now we set up the X and Y dimensions in pixels. To avoid stretched/squashed characters, the ration between X and Y must match the aspect ratio of the menu prim (see above). 512px is a good compromise between clarity and speed of texture loading.
Menu(MENU_TITLE, "DEMONSTRATION");
This sets up the title of the menu, which is displayed at the very top of the menu in white on blue.
Menu(MENU_DESCRIPTION, "Select scene to load:");
The description, like its analogue in the native LSL dialog function, appears above the buttons. This appears in blue on white. It is possible to add text to the description by repeated calls to this command, although at the time of writing this has not been thoroughly tested. Additional description text appears on separate lines.
Menu(MENU_SIDES, "1");
Now we specify which side(s) of the menu prim is to be used. Since the OpenSim drawing commands apply to all sides of a prim, the purpose of this is to specify which side(s) should be visible. All other sides are kept 100% transparent even when the menu is in use. Separate multiple side values with commas (eg "1,3,4").
Menu(MENU_SORT, "S"); // "smart sort"
By default, the menu buttons are displayed in the sequence in which they're added. Specifying a sort sequence overrides this behaviour; in this case we use "S" for "smart sort"; the alternatives are "N" for a normal sort, "R" for a reverse sort and "" for no sorting. A smart sort is like a normal sort but the (first) numerical portion of the text is sorted numerically rather than alphabetically - for example, [ "test 1", "test 2", "test 10" ] rather than [ "test 1", "test 10", "test 2" ].
key OwnerId = llGetOwner();
[...]
Menu(MENU_USER, (string)OwnerId);
This specifies that only the given user (in this case, the owner of the object) can respond to the menu. If this is not specified, anyone can use the menu. A reasonable enhancement would be to allow a list of users, or even a run-time hook for validation, but for now it's all or only one.
3. Adding buttons
To add a basic button, just use:
Menu(MENU_BUTTON, "Paris");
This adds a button with the text "Paris".
We can add options to buttons too. The text and options are comma-separated. Currently the only option is "S" for "stay" - this means that selecting this option will not make the menu close. The default behaviour is that clicking an option sends that button back to the application and closes the menu.
Menu(MENU_BUTTON, "Rome,S");
This will add a button with the text "Rome", which will not close the menu when clicked.
You can also specify the button colour, although currently only white text on a coloured background is supported. A reasonable future enhancement would be to allow buttons to be displayed in inverse - the coloured text on a white background with a narrow coloured border. The default colour is dark red.
The colour can be specified as a second comma-separated value, like this:
Menu(MENU_BUTTON, "Madrid,,DarkGreen");
which adds a button with the text "Madrid" in white on a dark green background. Colours may be specified as hex values in the format AARRGGBB, so that "FF00FF00" is bright green (always use FF for alpha). Alternatively (and advisedly) you can use one of the string literals specified in the .NET standards documented on this page, as in the Madrid example.
(Note that other elements are currently hard-wired for colour, although a reasonable enhancement would be to allow them to be changed, and even the white background/text part to be under application control.)
4. Displaying the menu
So far, we have only specified the makeup of the menu, and to the user, nothing has happened. In order to display the menu and open it up for input, we need to activate it:
Menu(MENU_ACTIVATE, "");
This will cause the mask prim to become full-size and visible for a few seconds (to give the menu texture time to draw and be downloaded and displayed by the users' viewers), and the menu itself to display full-size when the mask is withdrawn.
The user can now select options from the menu.
5. Handling options
Options selected from the menu are returned as link messages with the numeric value as specified above as MENU_RESPONSE. The text of the option selected is returned as the string portion of the link message, and the key portion contains the UUID of the avatar that selected it.
This sample code fragment handles the button clicks from the application's point of view (as well as handling a cancel event - see the section below on "Resetting"):
link_message(integer Sender, integer Number, string String, key Id) {
if (Number == MENU_RESPONSE) {
if (String == "Paris") {
GoTo("France");
}
else if (String == "Rome") {
GoTo("Italy");
}
[...]
else if (Number == MENU_CANCEL) {
Cancel();
}
6. Resetting
There is the possibility (as shown above) of the menus system returning MENU_CANCEL. This should not happen in normal operation, but if the menu script reset for any reason, this will be sent. It allows the application to recover from a state where it might be inactive while listening for a menu response. There is always the possibility of future causes of menu cancellation, so it's best to code for this now.
Likewise, when the application starts it should send MENU_RESET to the menu prim, in case the application script is reset while the menu is displayed. Without this, the object may be restarted while still displaying the menu from previous actions.
Between those two methods, stability and consistency is ensured when recovering from resets, crashes, etc of either the menu system or the application.