Chapter 25: Standard Window Menus

We’ve now covered two special types of menu – the iconbar menu and pop-up menu – but have yet to look at the simplest type: one that opens over a window following a click with the Menu button.

It’s now time for us to correct that omission, but first we will use some of our new-found knowledge to lay the foundations for our small application to do something useful. With the ability to select from a range of shapes, we could perhaps use it to do some simple geometric calculations.

Updating the templates

The first thing that we need to do is to update the window templates to include some new icons. Load !ExamplApp.Templates into your template editor, then open the “Main” window. As ever, we will be using WinEd in the description that follows.

We are going to add two new display fields, as seen in Figure 25.1. Expand the work area downwards, then drag a couple of display fields and labels across from the Icon picker into the positions shown.

If the work is done in WinEd, then the icons will be numbered 4 and 5 for the Sides display field and label respectively, then 6 and 7 for the Internal angles display field and label. These will be the numbers that we will use in our code below.

Both display field icons use the R validation command that we have met before, in Section 15.7 and Chapter 19, to create the 3D effect. Entering a default string of “000” into the Sides field ensures that WinEd configures a Max text length of 4 – allowing up to three digits and a terminator. In a similar way, the “00000000” in the Internal angles field ensures a length of 8 digits plus a terminator, or a total length of 9.

Close the window and save the changes to the templates file.

Figure 25.1: The window with two new result fields included

Adding a calculator

In order to be able to complete our two new fields, we will need some way to identify the number of sides for each of our shapes, and to calculate their internal angles. It’s usually a good idea to separate the code which does the actual work from the code which handles the user interface, so we will add another source file called c.calc and its associated header file h.calc. It will be necessary to update the OBJS list in the Makefile to include a reference to it.

OBJS = calc main menu ibar win

The new file will be a self-contained module for calculating simple geometry for the shapes that we are using. The h.calc header file can be seen in Listing 25.1, whilst the main code from c.calc can be seen in Listing 25.2. The new enum calc_shape will replace the enum win_shape which was previously defined in c.win.

#ifndef EXAMPLEAPP_CALC
#define EXAMPLEAPP_CALC

/* Display Shapes */

enum calc_shape {
        CALC_SHAPE_NONE,
        CALC_SHAPE_SQUARE,
        CALC_SHAPE_CIRCLE,
        CALC_SHAPE_TRIANGLE
};

/* Calculation Initialisation. */

void calc_initialise(void);

/* Shape Selection. */

void calc_set_shape(enum calc_shape shape);

/* Set the formatting of results. */

void calc_set_format(int places);

/* Get the decimal places. */

int calc_get_places(void);

/* The number of sides. */

char *calc_get_sides(void);

/* The internal angle. */

char *calc_get_internal_angle(void);

#endif

Listing 25.1 (h.calc): The new calculation header

/* Constants. */

#define CALC_BUFFER_LENGTH 32
#define CALC_FORMAT_LENGTH 8

/* Global Variables. */

static enum calc_shape calc_current_shape = CALC_SHAPE_NONE;

static int calc_current_sides = 0;

static int calc_current_places = 0;

static char calc_buffer[CALC_BUFFER_LENGTH];

static char calc_format[CALC_FORMAT_LENGTH];

static char *calc_not_applicable = "n/a";

/* Calculation Initialisation. */

void calc_initialise(void)
{
        calc_set_format(2);
        calc_set_shape(CALC_SHAPE_NONE);
}

/* Shape Selection. */

void calc_set_shape(enum calc_shape shape)
{
        calc_current_shape = shape;

        switch (shape) {
        case CALC_SHAPE_CIRCLE:
                calc_current_sides = 1;
                break;
        case CALC_SHAPE_TRIANGLE:
                calc_current_sides = 3;
                break;
        case CALC_SHAPE_SQUARE:
                calc_current_sides = 4;
                break;
        default:
                calc_current_sides = 0;
                break;
        }
}

/* Set the formatting of results. */

void calc_set_format(int places)
{
        if (places >= 0 && places <= 5) {
                calc_current_places = places;
                string_printf(calc_format, CALC_FORMAT_LENGTH, "%%.%df", places);
        }
}

/* Get the decimal places. */

int calc_get_places(void)
{
        return calc_current_places;
}

/* The number of sides. */

char *calc_get_sides(void)
{
        if (calc_current_sides > 1) {
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, "%d", calc_current_sides);
        } else {
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_not_applicable);
        }

        return calc_buffer;
}

/* The internal angle. */

char *calc_get_internal_angle(void)
{
        double angle;

        if (calc_current_sides > 2) {
                angle = ((calc_current_sides - 2.0) * 180.0) / calc_current_sides;
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, angle);
        } else {
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_not_applicable);
        }

        return calc_buffer;
}

Listing 25.2 (c.calc): The new calculation code

The code starts with some global constants and variables, which between them provide storage for the current shape and formatting choices. There are two buffers: one to hold the most recently generated output text, and the other to hold a printf() format string.

The calc_initialise() function needs to be called from main_initialise() in order to set up the variables and buffers to some sensible defaults, after which calc_set_shape() and calc_set_format() can be used to choose the active shape and set the number of decimal places used when displaying numbers. The configured number of decimal places can be read back using the calc_get_places() function.

The other two functions are calc_get_sides() and calc_get_internal_angle(), which return a pointer to the output buffer having written a suitable string into it given the current shape. To avoid getting into geometric arguments, this will be “n/a” for the circle, and the calculated values for the triangle and square.

We can now modify the win_set_shape() function in c.win as shown in Listing 25.3, so that it calls a new win_update_all_calculations() function to make use of the new facilities.

static void win_set_shape(enum calc_shape shape)
{
        char *sprite = NULL;

        /* Update the graphic. */

        switch (shape) {
        case CALC_SHAPE_SQUARE:
                sprite = "square";
                break;
        case CALC_SHAPE_CIRCLE:
                sprite = "circle";
                break;
        case CALC_SHAPE_TRIANGLE:
                sprite = "triangle";
                break;
        }

        if (sprite != NULL) {
                icons_strncpy(WIN_HANDLE, win_icon_shape, sprite);
                wimp_set_icon_state(win_handle, WIN_ICON_SHAPE, 0, 0);
        }

        /* Change the shape. */

        calc_set_shape(shape);

        /* Perform the shape calculations. */

        win_update_all_calculations();
}

/* Update the shape data fields. */

static void win_update_all_calculations(void)
{
        char *text = NULL;

        text = calc_get_sides();
        if (text != NULL) {
                icons_strncpy(win_handle, WIN_ICON_SIDES_FIELD, text);
                wimp_set_icon_state(win_handle, WIN_ICON_SIDES_FIELD, 0, 0);
        }

        text = calc_get_internal_angle();
        if (text != NULL) {
                icons_strncpy(win_handle, WIN_ICON_INT_ANGLE_FIELD, text);
                wimp_set_icon_state(win_handle, WIN_ICON_INT_ANGLE_FIELD, 0, 0);
        }
}

Listing 25.3: Upodating the data fields

The function now takes a parameter of the new enum calc_shape type, so the calls made to it will need to be updated accordingly.

When compiled and run, our application window should look similar to that shown in Figure 25.2. Choosing different shapes from the pop-up menu should result in the Sides and Internal angles fields updating to show appropriate values.

Figure 25.2: The application can now show some information about the displayed shape

The full code of the changes can be found in Download 25.1.

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

A window menu

When adding our calculation facility, we included support for setting the number of decimal places to which values will be displayed. This doesn’t actually make a lot of difference for the shapes and data that we are able to show – although it would become relevant if we extended the shapes to include a heptagon, where the internal angle isn’s a round number of degrees. What we can’t do at present is change this value without recompiling our code.

An obvious solution would be to provide a menu which contains these options, so we will do just that. It’s possible to imagine further ‘behavioural’ parameters that we might wish to adjust in the future, so we will create a general menu for the window to hold them all. We can start by adding some constants to c.win which define the menu entries as seen in Listing 25.4 and a new global variable for the wimp_menu block pointer as in Listing 25.5.

/* Window Menu Entries. */

#define WIN_MENU_DECIMAL1 0
#define WIN_MENU_DECIMAL2 1
#define WIN_MENU_DECIMAL3 2

Listing 25.4: Constants to define the window menu entries

/* Global Variables */

static wimp_w win_handle;
static wimp_menu *win_menu;
static wimp_menu *win_shape_menu;

Listing 25.5: A new global variable to hold a pointer to the menu

The menu itself can then be defined in the win_initialise() function as shown in Listing 25.6, in a way which should now be familiar.

/* Window Menu. */

win_menu = menu_create("Example", 3);
if (win_menu == NULL) {
        error_report_error("Failed to create Main Menu");
        return;
}

menu_entry(win_menu, WIN_MENU_DECIMAL1, "1 Decimal place", NULL);
menu_entry(win_menu, WIN_MENU_DECIMAL2, "2 Decimal places", NULL);
menu_entry(win_menu, WIN_MENU_DECIMAL3, "3 Decimal places", NULL);

Listing 25.6: Defining the main window menu

Now that we have the new menu defined, we can open it when the user clicks Menu over the window. So far, we have created functions to open menus over the iconbar and from pop-up menu fields, but do not yet have a way to open a menu at the mouse pointer. To rectify this, we can add another function to c.menu which will open a menu in the standard position, as shown in Listing 25.7.

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

        menus_create_standard_menu(menu, pointer);
}

Listing 25.7: A function to open a standard menu

As before, this simply defers to SFLib’s menus_create_standard_menu() function to display the menu. When opening a menu at the mouse pointer as a result of a Menu click, the process is a lot easier that the other situations that we have seen so far. The Style Guide requires that it is opened at the y coordinate of the click, and 64 OS units to the left of the x coordinate; the code that SFLib uses to achieve this can be seen in Listing 25.8.

wimp_menu *menus_create_standard_menu(wimp_menu *menu, wimp_pointer *pointer)
{
        if (menu == NULL || pointer == NULL)
                return NULL;

        if (xwimp_create_menu(menu, pointer->pos.x - 64, pointer->pos.y) != NULL)
                return NULL;

        return menu;
}

Listing 25.8: The code used by SFLib to position a standard menu

Armed with the new menu_open() function, we can update the win_mouse_click() event handler as shown in Listing 25.9, so that Menu clicks anywhere over the window will cause the main menu to be opened.

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);
        } else if (pointer->buttons == wimp_CLICK_MENU) {
                win_set_menu();
                menu_open(win_menu, pointer, win_menu_selection);
        }
}

Listing 25.9: Menu click events over the window will open the main menu

Before the menu is opened, the win_set_menu() function in Listing 25.10 is called to place a tick against the currently selected choice of decimal places. This reads the configured number of places back from the calc module first.

static void win_set_menu(void)
{
        int places;

        places = calc_get_places();

        menus_tick_entry(win_menu, WIN_MENU_DECIMAL1, places == 1);
        menus_tick_entry(win_menu, WIN_MENU_DECIMAL2, places == 2);
        menus_tick_entry(win_menu, WIN_MENU_DECIMAL3, places == 3);
}

Listing 25.10: Updating the main menu ready for display

The final piece of the jigsaw is the Menu_Selection event handler in win_menu_selection(), which can be seen in Listing 25.11. This sets the required number of decimal places in the calc module, before calling win_update_all_calculations() to update the display fields with the new settings. Finally win_set_menu() is called, so that the tick can be updated before the menu is reopened if Adjust was clicked.

static void win_menu_selection(wimp_menu *menu, wimp_selection *selection)
{
        switch (selection->items[0]) {
        case WIN_MENU_DECIMAL1:
                calc_set_format(1);
                break;
        case WIN_MENU_DECIMAL2:
                calc_set_format(2);
                break;
        case WIN_MENU_DECIMAL3:
                calc_set_format(3);
                break;
        }

        win_update_all_calculations();
        win_set_menu();
}

Listing 25.11: Processing selections from the main menu

If the code is compiled and the application run, the menu should appear as seen in Figure 25.3. Selecting different numbers of places from the menu should update the number in the Internal angles field, assuming that shape isn’t a circle!

Figure 25.3: The main menu allows the decimal places to be changed

The full code and templates can be found in Download 25.2.

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

Updating menus

Now that we have two menus in our window, both of which are opened following clicks handled in win_mouse_click(), observant readers might have noticed a difference between the way that the two are treated.

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);
        } else if (pointer->buttons == wimp_CLICK_MENU) {
                win_set_menu();
                menu_open(win_menu, pointer, win_menu_selection);
        }
}

Both menus currently contain three entries, of which one can be ticked at any given time. However, whilst both of the two Menu_Selection handlers – in win_menu_selection() and win_shape_menu_selection() – call their respective routines to update the ticks in case the menu is reopened by an Adjust click, this isn’t true of when the menus are initially opened.

Before the main menu, whose pointer is in win_menu is opened with a call to menu_open(), there is a call made to win_set_menu() first to ensure that the tick is in the correct place. However, when the pop-up menu whose handle is in win_shape_menu is opened, there is no set-up done before menu_open_popup() is called. Instead the code relies on the fact that win_set_shape_menu() was called by the win_initialise() function, and will be left up-to-date each time a selection is made.

There is actually a good reason for this in our example, because there is no way for the pop-up menu’s set routine to read the shape that is currently active. When the window is initialised or a menu selection made, the shape and the ticks are set to match each other and the calc module is informed of the change through the calc_set_shape() function. Since we haven’t defined a corresponding calc_get_shape() function, it wouldn’t be possible for win_set_shape_menu() to read the current shape back in the way that win_set_menu() does for the decimal places.

Which approach to use is largely down to the developer, and different situations may well suggest different solutions. In general, your author would probably recommend using the approach taken by win_set_menu() and setting all of the ticks, shadings and other configurable aspects of a menu each time it is opened and reopened on screen. This keeps all of the associated code in one place, and can be much easier to maintain than if each separate routine updates the parts of the menu that it uses directly.

In addition, as we will see in the next chapter, this is the approach that SFLib supports with its own menu handling.