Chapter 24: Pop-Up Menus and Other Features

In addition to conventional menus accessed by the Menu button on the mouse, RISC OS allows for the use of pop-up menus in dialogue boxes. Whilst technically the same, they behave in a specific way and are usually used to replace groups of radio icons in dialogue boxes. An example from SciCalc’s Calculator choices can be seen in Figure 24.1.

Figure 24.1: Pop-up menus in use within the SciCalc Choices dialogue

In terms of implementation, a pop-menu is simply a display field icon and label of the kind that we have met already, with a pop-up menu button to their right. A standard menu contains the options which can be selected – it is up to the application to watch for Select clicks on the pop-up menu button, then open a menu in the appropriate place and with the currently selected item ticked. When the user makes a new selection, it is also up to the application to update the contents of the display field. In fact, the Wimp will do very little for us at all!

The three radio icons in our main window, which we use to choose a shape for display, would be good candidates for replacement with a pop-up menu – especially if we ever plan to add any more shapes to our application. In this chapter, we will investigate how this can be done.

Updating the templates

The first step towards implementing our pop-up menu is to update the window template so that it contains the icons needed for a pop-up menu field. Load !ExamplApp.Templates into a template editor, and open the “Main” template. Delete the three radio icons from the window.

In many template editors, this should automatically renumber the sprite icon containing the square to be icon 0 because there are no other icons left in the window. If it does not, then use the renumbering facility to change the value; in WinEd, click Menu over the icon, slide over Icon → Renumber → to the Renumber icon dialogue box, enter 0 into the field and click on Renumber. The icon number can be found by hovering the mouse over the icon and looking at the Monitor, as seen in Figure 24.2.

Figure 24.2: The icon with the square sprite should be number 0

We can now add a pop-up menu field below the square. Select the Display field in the Icon picker, add in the Comment to its left with an Adjust click, then also include the pop-up menu button to its right with another Adjust click. The three icons can then be dragged over to the window template as a single selection, and placed below the square.

The text in the label can be changed from “Comment” to “Shape” in the usual way, and with a bit of rearranging and adjusting of the window’s work area extents (using the Work area dialogue that we met in Chapter 21), we should end up with a result similar to that seen in Figure 24.3. The icons should be numbered such that the pop-up menu icon is number 1, the display field is number 2 and the label is number 3 – this should be how WinEd will number them from the Icon picker anyway.

Figure 24.3: Updating our window template for a pop-up menu

It’s worth having a look at the pop-up menu icon (number 1) before we move on, because it uses a combination of options that we haven’t yet met. The button has a type of wimp_BUTTON_CLICK so that clicks are reported back to our application through the usual Mouse_Click event. It uses a pair of standard sprites from the Wimp Sprite Pool – “gright” and “pgright” – which are supplied by the system for this purpose and represent raised and pressed versions of a pop-up menu button. The name comes from history: the original RISC OS 3.1 design was a flat, grey arrow which pointed to the right; the “p” is a standard convention which indicates “pressed”.

Despite containing only a sprite, the icon is configured for indirected text and sprite but the text field is left as an empty string. This allows a validation string to be specified, because the effect that we are after can only be achieved through the use of validation commands. The S command that we met in Chapter 18 is used to specify the two sprites, whilst the R command from Section 15.7 is used to make the icon a 3D ‘clickable’ button which responds to the mouse. The icon has no border, however (the wimp_ICON_BORDER flag is not set), so no 3D border is plotted; when the button ‘clicks’, the only effect seen by the user is the sprite changing from unselected to selected and back again. The complete validation string is R5;Sgright,pgright.

With the design updated, close the window and save the changes to the templates file.

Adding some code

To implement our new pop-up menu, there are a few things that we will need to add to our code – not least a way to open the menu in the correct place on screen. In the previous chapter we implemented the menu_open_ibar() function, but this was coded specifically for handling the positioning of iconbar menus and the requirements for pop-up menus are very different.

Following the same naming and parameter format, we can add a new menu_open_popup() to c.menu as shown in Listing 24.1 (with the appropriate prototype in h.menu):

void menu_open_popup(wimp_menu *menu, wimp_pointer *pointer,
                void (*callback)(wimp_menu *menu, wimp_selection *selection))
{
        menu_current_menu = menu;
        menu_current_callback = callback;

        menus_create_popup_menu(menu, pointer);
}

Listing 24.1: A function to open a pop-up menu

As with menu_open_ibar(), this sets the global variables used to track the currently open menu, before using SFLib’s menus_create_popup_menu() function to put the menu on to the screen. The code that SFLib uses is shown in Listing 24.2.

wimp_menu *menus_create_popup_menu(wimp_menu *menu, wimp_pointer *pointer)
{
        wimp_window_state       window;
        wimp_icon_state         icon;

        if (menu == NULL || pointer == NULL)
                return NULL;

        window.w = pointer->w;
        if (xwimp_get_window_state(&window) != NULL)
                return NULL;

        icon.w = pointer->w;
        icon.i = pointer->i;
        if (xwimp_get_icon_state(&icon) != NULL)
                return NULL;

        if (xwimp_create_menu(menu, window.visible.x0 + icon.icon.extent.x1 - window.xscroll,
                        window.visible.y1 + icon.icon.extent.y1 - window.yscroll) != NULL)
                return NULL;

        return menu;
}

Listing 24.2: The code used by SFLib to position a pop-up menu

This is a little more complex than positioning an iconbar menu. The Style Guide states that a pop-up menu must “appear immediately to the right of the button the user clicked on to display it”, so the mouse coordinates in the wimp_pointer structure supplied by the Mouse_Click event are of little use to us. The structure also contains the wimp_w and wimp_i handles of the button that was clicked on, but we don’t know where that button is on screen.

The routine starts by calling Wimp_GetWindowState to find the scroll offsets of the window and the coordinates of its visible area relative to the screen origin, before calling Wimp_GetIconState to retrieve the location of the target icon in terms of the window’s work area. Now that we know where the icon is within its window, and also where that window is on screen, we can combine the two pieces of information to work out where the icon is relative to the screen origin.

For the horizontal coordinate, the position of the right-hand edge of the icon relative to the screen origin can be calculated by adding the position of the left-hand edge of the window’s visible area relative to the screen origin to the position of the right-hand edge of the icon relative to the window’s work area. If the window has been scrolled horizontally then this will move the icon towards the screen origin, but we can take account of this by subtracting the horizontal scroll offset. This can be seen graphically in Figure 24.4.

The calculation for the vertical coordinate is similar, except that as we saw in Section 12.2, the Wimp conventionally stores the vertical positions of icons as negative values referenced to the top of the window’s work area. This makes the calculation the same as for the horizontal position: the position of the icon relative to the screen origin is calculated by taking the position of the top edge of the window’s visible area relative to the screen origin and adding the (negative) position of the top of the icon relative to the window’s work area. If the window has been scrolled vertically then this will move the icon away from the screen origin, but once again we can take account of this by subtracting the (negative) vertical scroll offset. Again, this can be seen graphically in Figure 24.4.

Figure 24.4: Calculating the position of a pop-up menu

With a means of opening a pop-up menu in place, we can update the c.win file to make use of it. We will begin by updating the constant definitions at the top of the file as shown in Listing 24.3, to reflect both the changes to the icons in the window template and the new menu that we will be creating.

/* Main Window Icons. */

#define WIN_ICON_SHAPE 0
#define WIN_ICON_SHAPE_POPUP 1
#define WIN_ICON_SHAPE_FIELD 2

/* Shapes Menu Entries. */

#define WIN_MENU_SHAPE_CIRCLE 0
#define WIN_MENU_SHAPE_TRIANGLE 1
#define WIN_MENU_SHAPE_SQUARE 2

Listing 24.3: The revised icon and menu entry constants

We will also need the global variable shown in Listing 24.4 to hold a pointer to our menu definition, alongside that holding our window handle.

/* Global Variables */

static wimp_w win_handle;
static wimp_menu *win_shape_menu;

Listing 24.4: The global variable to hold the pop-up menu struct pointer

The win_initialise() function sees some changes. After loading the window template, we will need to create the new shapes menu as shown in Listing 24.5, using a similar approach to that taken with the iconbar menu in Chapter 22.

/* Shapes Menu. */

win_shape_menu = menu_create("Shapes", 3);
if (win_shape_menu == NULL) {
        error_report_error("Failed to create Shapes Menu");
        return;
}

menu_entry(win_shape_menu, WIN_MENU_SHAPE_CIRCLE, "Circle", NULL);
menu_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE, "Triangle", NULL);
menu_entry(win_shape_menu, WIN_MENU_SHAPE_SQUARE, "Square", NULL);

Listing 24.5: Creating the new pop-up menu data

This will give us a menu with three entries: one for each of the shapes that we have at our disposal. Finally, we can remove the code that we had for configuring the radio icons, leaving us with just the call to win_set_shape() from the original code.

The code that we used to handle the radio icons in win_mouse_click() can be removed, to be replaced by the code in Listing 24.6 to detect Select clicks on the new pop-up menu icon and open the pop-up menu.

static void win_mouse_click(wimp_pointer *pointer)
{
        if (pointer->i == WIN_ICON_SHAPE_POPUP && pointer->buttons == wimp_CLICK_SELECT)
                menu_open_popup(win_shape_menu, pointer, win_shape_menu_selection);
}

Listing 24.6: The updated Mouse Click event handler

The code which was selecting the required shape based on clicks on the radio icons can be converted into a Menu_Selection event handler in win_shape_menu_selection(), shown in Listing 24.7.

static void win_shape_menu_selection(wimp_menu *menu, wimp_selection *selection)
{
        switch (selection->items[0]) {
        case WIN_MENU_SHAPE_CIRCLE:
                win_set_shape(WIN_SHAPE_CIRCLE);
                break;
        case WIN_MENU_SHAPE_TRIANGLE:
                win_set_shape(WIN_SHAPE_TRIANGLE);
                break;
        case WIN_MENU_SHAPE_SQUARE:
                win_set_shape(WIN_SHAPE_SQUARE);
                break;
        }
}

Listing 24.7: The new Menu Selection event handler

Finally, our win_set_shape() can be extended as shown in Listing 24.8 so that it updates the pop-up menu field as well as changing the shape being displayed.

static void win_set_shape(enum win_shape shape)
{
        char *sprite = NULL, *name = NULL;

        switch (shape) {
        case WIN_SHAPE_SQUARE:
                sprite = "square";
                name = "Square";
                break;
        case WIN_SHAPE_CIRCLE:
                sprite = "circle";
                name = "Circle";
                break;
        case WIN_SHAPE_TRIANGLE:
                sprite = "triangle";
                name = "Triangle";
                break;
        }

        if (sprite == NULL)
                return;

        icons_strncpy(WIN_HANDLE, win_icon_shape, sprite);
        wimp_set_icon_state(win_handle, WIN_ICON_SHAPE, 0, 0);

        icons_printf(win_handle, WIN_ICON_SHAPE_FIELD, "%s", name);
        wimp_set_icon_state(win_handle, WIN_ICON_SHAPE_FIELD, 0, 0);
}

Listing 24.8: Updating win_set_shape() to fill the pop-up menu field

When run, we should find that our application has a functioning pop-up menu as seen in Figure 24.5.

Figure 24.5: Our pop-up menu field allows shapes to be selected

This isn’t a completely satisfactory solution, for a couple of reasons: it blurs the boundary between the icon with the square in it and the pop-up menu used to choose the shape, and there seem to be a lot of copies of the shape names dotted around in the code as string constants. We’ll deal with these worries later on, though – for the time being, the code can be found in Download 24.1.

Download 24.1
The source code and files in this example are made available under Version 1.2 of the European Union Public Licence.

Ticking menu entries

One obvious thing that is wrong with our example is that it doesn’t indicate the currently selected item with a tick in the pop-up menu. This is a fairly standard requirement, at least for menus where there is a direct relationship between entries in the menu and the pop-up menu field to which it is attached.

An entry in a menu will be displayed with a tick to the side if the wimp_MENU_TICKED flag is set in the menu_flags field of the wimp_menu_entry structure. Changing its state is easier than making similar changes to an icon’s flags, because as we’ve already seen, the Wimp works directly from the wimp_menu structure in memory when displaying menus. So long as we have updated the flags in the structure before calling Wimp_CreateMenu, the changes will take effect as required.

The actual changes to the flags require some bitwise calculations, so SFLib provides the menus_tick_entry() function shown in Listing 24.9 to set the state of an entry’s wimp_MENU_TICKED flag. It should be passed a pointer to the wimp_menu structure containing the menu definition, the index of the menu entry, and either TRUE to add a tick or FALSE to remove one. Note that there is no bounds checking on the value passed to entry!

void menus_tick_entry(wimp_menu *menu, int entry, osbool tick)
{
        if (menu == NULL)
                return;

        if (tick)
                menu->entries[entry].menu_flags |= wimp_MENU_TICKED;
        else
                menu->entries[entry].menu_flags &= ~wimp_MENU_TICKED;
}

Listing 24.9: SFLib's routine for ticking a menu entry

Armed with this function, we can write a new win_set_shape_menu() function as shown in Listing 24.10 to update the ticks in the menu. Unlike the radio icons, where the Wimp ensured that only one was selected at any time, with a menu we must go to each entry in turn and explicitly tick or un-tick it.

static void win_set_shape_menu(enum win_shape shape)
{
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_CIRCLE,
                        shape == WIN_SHAPE_CIRCLE);
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE,
                        shape == WIN_SHAPE_TRIANGLE);
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_SQUARE,
                        shape == WIN_SHAPE_SQUARE);
}

Listing 24.10: Setting the state of the pop-up menu ticks

We can now call this new function from the end of the win_set_shape() function, and the menu will be set up every time the shape changes. The code can be found in Download 24.2.

Download 24.2
The source code and files in this example are made available under Version 1.2 of the European Union Public Licence.

If the application is compiled and run, the result should be as seen in Figure 24.6 – opening the menu and selecting different shapes with both Select and Adjust should reveal that the selection is both initialised correctly and updated when the selection changes, even if the menu remains open.

This works because we call win_set_shape_menu() from the win_set_shape() function, which in turn is called to initialise the shape and then called again from the win_shape_menu_selection() event handler whenever the shape changes. The c.menu code calls the handler before using Wimp_CreateMenu to re-open the menu on an Adjust click, so the tick will be moved by our code before the menu is recreated by the Wimp.

Things are made a little easier for our code because we know that the only way that the shape can change is through the user making a selection from the pop-up menu. If this was not the case (such as if there were still radio icons in the window with the same purpose), then we would need to be careful that any change made in other ways also updated the menu – or updated the menu before it was initially opened. We will return to look at these options in a later chapter.

Figure 24.6: A tick in the pop-up menu shows the current selection

Shading entries

In addition to allowing applications to tick menu entries, the Wimp provides a means of shading them if they are not applicable or can not be selected for some reason. This is done in a similar way to adding a tick, but since the shading is controlled through the standard icon flags, a menu entry is shaded if the wimp_ICON_SHADED flag is set in the icon_flags field of the wimp_menu_entry structure. Again, the fact that Wimp_CreateMenu works directly from the memory structure means that changes are easy to make.

SFLib provides the menus_shade_entry() function shown in Listing 24.11 to change the state of the wimp_ICON_SHADED flag for a menu entry. It should be passed a pointer to the wimp_menu structure containing the menu definition, the index of the menu entry, and either TRUE to shade the entry or FALSE to enable it. As with the menus_tick_entry() function, there is no bounds check done on the entry parameter.

void menus_shade_entry(wimp_menu *menu, int entry, osbool shade)
{
        if (menu == NULL)
                return;

        if (shade)
                menu->entries[entry].icon_flags |= wimp_ICON_SHADED;
        else
                menu->entries[entry].icon_flags &= ~wimp_ICON_SHADED;
}

Listing 24.11: SFLib's routine for shading a menu entry

Using the function to shade menu entries is straight-forward. Taking a slightly contrived example, suppose that we wished to enforce a rule that after selecting a square, the user could only select a circle. We could do this by updating win_set_shape_menu() as shown in Listing 24.12 so that in addition to setting the various ticks, it also calls the menus_shade_entry() to shade the Triangle entry when required.

static void win_set_shape_menu(enum win_shape shape)
{
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_CIRCLE,
                        shape == WIN_SHAPE_CIRCLE);
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE,
                        shape == WIN_SHAPE_TRIANGLE);
        menus_tick_entry(win_shape_menu, WIN_MENU_SHAPE_SQUARE,
                        shape == WIN_SHAPE_SQUARE);

        menus_shade_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE,
                        shape == WIN_SHAPE_SQUARE);
}

Listing 24.12: Shading a menu entry

When run, this will result in the Triangle entry becoming unavailable if the current shape is a square, as seen in Figure 24.7. The code is avaulable in Download 24.3.

Figure 24.7: Menu entries can be shaded to prevent the user choosing them

Note that it is up to our application to ensure that a menu entry isn’t both ticked and shaded, unless such a combination proved to be valid in a specific context, and the code above meets this requirement.

Download 24.3
The source code and files in this example are made available under Version 1.2 of the European Union Public Licence.

Reading menu texts

We mentioned in Section 24.3 that there was a bit of duplication going on as far as string constants in our code was concerned. Specifically, we defined the pop-up menu entries in the win_initialise() function:

menu_entry(win_shape_menu, WIN_MENU_SHAPE_CIRCLE, "Circle", NULL);
menu_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE, "Triangle", NULL);
menu_entry(win_shape_menu, WIN_MENU_SHAPE_SQUARE, "Square", NULL);

However, we then went on to set the text in the pop-up menu field in the win_set_shape() function by specifying the constants again (the code for updating the sprite icon has been removed):

char *name = NULL;

switch (shape) {
case WIN_SHAPE_SQUARE:
        name = "Square";
        break;
case WIN_SHAPE_CIRCLE:
        name = "Circle";
        break;
case WIN_SHAPE_TRIANGLE:
        name = "Triangle";
        break;
}

icons_printf(win_handle, WIN_ICON_SHAPE_FIELD, "%s", name);
wimp_set_icon_state(win_handle, WIN_ICON_SHAPE_FIELD, 0, 0);

This works, and in fact the compiler may well optimise out the extra string constants when it compiles the code. It isn’t great code, however, and it would be neater if we only defined the text in one place. One possible solution would be to read the text from the appropriate pop-up menu entry when updating the pop-up menu field.

Reading a menu text sounds simple, but it isn't quite as straight-forward as it might seem. For non-indirected menu entries, the text is held in the 12 bytes of icon data within the wimp_menu_entry structure, but if the entry is indirected then we need to look in the location indicated by the pointer stored at this address. SFLib provides a function to return a pointer to the text, in the form of menus_get_text_addr() seen in Listing 24.13.

char *menus_get_text_addr(wimp_menu *menu, int entry)
{
        if (menu == NULL)
                return NULL;

        if (menu->entries[entry].icon_flags & wimp_ICON_INDIRECTED)
                return menu->entries[entry].data.indirected_text.text;
        else
                return menu->entries[entry].data.text;
}

Listing 24.13: SFLib's routine for reading the addres of a menu text

This function takes a pointer to the wimp_menu structure and the index of the menu entry for which we require the text, and returns a pointer to the string (or NULL if we pass it a NULL menu pointer). We could use this to update our win_set_shape_menu() function, but instead we will take a slightly different route and create a new menu_set_popup_menu() function in c.menu as shown in Listing 24.14.

void menu_set_popup_menu(wimp_menu *menu, int selection, wimp_w window, wimp_i icon)
{
        wimp_menu_entry *entries = NULL;
        int             entry = 0;

        if (menu == NULL)
                return;

        /* Set the ticks on the menu entries. */

        entries = menu->entries;

        do {
                menus_tick_entry(menu, entry, selection == entry);
        } while (!(entries[entry++].menu_flags & wimp_MENU_LAST));

        /* Update the pop-up menu field to the current selection. */

        if (selection >= 0 && selection < entry) {
                icons_strncpy(window, icon, menus_get_text_addr(menu, selection));
                wimp_set_icon_state(window, icon, 0, 0);
        }
}

Listing 24.14: A new routine to set the ticks in any pop-up menu

A number of parameters are required, starting with a pointer to the wimp_menu definition for the pop-up menu and the index of the selection. These are followed by the wimp_w handle of the window containing the pop-up menu field associated with the menu, and the wimp_i handle of the icon.

The win_set_shape_menu() function that we defined back in Section 24.4 had a call to menus_tick_entry() for each entry in the menu, setting the tick according to the shape that was selected. This worked but, to follow a general theme, it wasn’t very expandable: if we added more entries to the menu, by adding more shapes, then the win_set_shape_menu() function would have needed to be updated to include more tests.

In contrast, the new function contains a do while loop which steps through all of the entries in the wimp_menu structure, setting the ticked state of each depending on whether its index matches the selection. This will work unaltered with a menu of any length.

Once the ticks have been set correctly, the function moves on to find a pointer to the text of the menu entry, and uses this to update the contents of the pop-up menu field icon given the the third and fourth parameters. The check that selection is at least zero and less than entry ensures that we don’t attempt to find a string pointer from outside of the array of wimp_menu_entry structures.

Returning to c.win, we can change the definition of win_set_shape_menu() so that it takes the menu selection as an int parameter instead of taking the shape in the form of an enum win_shape. It can then call menu_set_popup_menu() to do the hard work, as shown in Listing 24.15.

static void win_set_shape_menu(int selection)
{
        menu_set_popup_menu(win_shape_menu, selection, win_handle, WIN_ICON_SHAPE_FIELD);
}

Listing 24.15: The slimmed-down win_set_shape_menu() function

Next, we will move the call to our new win_set_shape_menu() function from win_set_shape() to the end of the win_shape_menu_selection() event handler, and use the selection supplied by the Wimp as shown in Listing 24.16.

win_set_shape_menu(selection->items[0]);

Listing 24.16: Updating the menu on a selection

To ensure that the pop-up menu and its field are correctly initialised on start-up, we will need to add a call to win_set_shape_menu() from the win_initialise() as shown in Listing 24.17, at the same time that we call win_set_shape().

win_set_shape(WIN_SHAPE_SQUARE);
win_set_shape_menu(WIN_MENU_SHAPE_SQUARE);

Listing 24.17: Initialising the menu and shape icon

When compiled and run, there should be no changes to the user experience compared to the application that we saw previously in Figure 24.6. It should be seen that the code to update the pop-up menu is a lot more generic than it was before – in fact, we now have a function which will update any pop-up menu that follows our design guidelines.

The full code can be found in Download 24.4.

Download 24.4
The source code and files in this example are made available under Version 1.2 of the European Union Public Licence.