Chapter 27: Writable Icons

Aside from a brief look at writable icons back in Chapter 15, our interaction with the Wimp has so far been entirely based around the mouse and pointer. This is fine for many simple applications, but sooner or later we will need our users to type something on their keyboard.

Following the changes that we made in Chapter 25, we have had fields to display the number of sides and the internal angles of the selected shape. It would be useful if the user could enter other parameters for the shape, such as the length of a side or the internal area, and see how they interact. We will now look at how this can be done.

From a user’s point of view, the Wimp handles keypresses in a fairly standard way. At any given time, one window on the desktop can have input focus, which means that it will receive input from the keyboard. The window with focus is shown by its title bar being highlighted in a different colour, although as we saw in Section 22.4, this is normally disabled for windows which are part of a menu tree. Figure 27.1 shows two Edit windows side by side; the one on the left has input focus.

Figure 27.1: Two edit windows: the left-hand one has input focus

From the point of view of the Wimp, a window has input focus if it contains the caret. This is a cursor – which may or may not be visble to the user – indicating where the next keypress will be ‘inserted’. It has a physical position within the window, which – just like icons – is calculated in OS units relative to the work area origin. The caret can be seen in the left-hand window of Figure 27.1 above. If a window has focus, its wimp_WINDOW_HAS_FOCUS window flag (which we met briefly in Section 13.5) will be set and this can be read using Wimp_GetWindowState.

If an application chooses to make the caret visible, it appears as a vertical red bar with serifed ends; the height is configurable, to suit different font sizes. Alternatively, an application may prefer to keep the caret hidden and use its own cursor – the Zap and StrongED text editors do this, for example, as seen in Figure 27.2. It might even be valid to give the user no visibility of the caret position, if keypresses are not being used to edit text.

Figure 27.2: An application can hide the caret if it wishes to

An application can handle the incoming keypresses itself, inserting characters, performing edits and moving the caret as required. This will be the approach taken by software such as wordprocessors and text editors, but it can involve a fair bit of work. An easier approach, and the one that we will start by looking at here, is to hand the problem back to the Wimp by using a writable icon: one with a type of wimp_BUTTON_WRITABLE. An example can be seen in Figure 27.3.

Figure 27.3: In a writable icon, the Wimp handles the caret for us

If the user clicks with in a writable icon, the Wimp will place the caret in an appropriate position within the icon’s text and then handle all of the usual input and editing options, including support for the global clipboard. The application can read the text back from the icon at any time, and in addition, the Wimp will pass on some of all of the keypresses for it to process if it needs to do so.

One final point to note is that the Style Guide states that the caret should be hidden if text is currently selected: in effect, the selection becomes the insertion point for the next keypress, as anything which is typed will replace it. This can be seen in Figure 27.4. Once again, the Wimp will handle this for us in writable icons.

Figure 27.4: The caret should become invisible if there is a selection

There’s a second variant of writable icon with the type wimp_BUTTON_WRITE_CLICK_DRAG, which reports drag events to the task in addition to single clicks. Wimp-handled drags within the icon (such as for text selection) may not work with icons of this type on some versions of RISC OS, so in most cases wimp_BUTTON_WRITABLE is recommended for the best compatibility.

New templates

We will start by updating the !ExamplApp.Templates file to add three writable fields to the main window, as seen in . The work area has been extended downwards to make space for the fields, which are made from the Writable icon and Comment icons in the WinEd Icon picker.

The icon numbers should continue to increase downwards from the Internal angles display field, which is number 6, and its label, which is number 7. The Side length field and its label are icon numbers 8 and 9; the Perimeter field and its label are icon numbers 10 and 11; the Area field and its label are icon numbers 12 and 13. For the first time, the order of the icon numbers in the window is important beyond ensuring that the template agrees with the constants in our code, because it will affect the use of the keyboard to navigate between the writable fields.

Figure 27.5: The window template with three writable fields added

The labels are standard comment icons, like the others already in the window; the template editor will make them indirected for us if the text is longer than 12 characters. The writable fields are something new, however – and the definition can be seen in Figure 27.6.

The Button type of the icon is set to “Writable”, which corresponds to the wimp_BUTTON_WRITABLE value in the icon flags. The wimp_ICON_TEXT and wimp_ICON_INDIRECTED flags are set, so that the text in the icon can be changed: a non-indirected writable icon would be fairly useless! The appearance of a writable icon is fairly strictly controlled by the Style Guide: a black foreground and white background, with a border and no 3D effects. The ESG is set to 0.

The validation string for the icon is “Pptr_write;Kta;A0-9.” – some of which should already be familiar, since we met the P and A commands back in Chapter 15. The K command is one that we have not yet met: it is almost always specified for writable icons, and we will find out what it does later in the chapter.

The “Pptr_write” command tells the Wimp to use the “ptr_write” sprite from the Wimp Sprite Pool as the mouse pointer when the cursor is over the icon – which, as we have previously mentioned, is a standard requirement in the Style Guide. The A command is applying a filter to the characters which can be typed into the field: we will be expecting non-negative numbers, so the digits 0 to 9 and a decimal point are all that we want the user to be able to type. See Section 15.5 for a reminder of how these filters work.

Figure 27.6: The definition for one of the writable field icons

When setting the icons up, it might be easiest to place one field and configure the writable icon as above, before copying it to create the other two fields. Once the window is set up as above, save the template file ready to use in the application.

Extra calculations

In order to make our new icons work, we will need to add some additional functionality to the c.calc and h.calc files. First, we will define some dimension types in the header file, as shown in Listing 27.1. These will allow us to tell the code which of the fields the user has specified as the base for calculations.

/* Dimension Types */

enum calc_dimension {
        CALC_DIMENSION_NONE,
        CALC_DIMENSION_LENGTH,
        CALC_DIMENSION_DIAMETER,
        CALC_DIMENSION_PERIMETER,
        CALC_DIMENSION_AREA
};

Listing 27.1: The types of dimension that we will be able to calculate

Over in c.calc, we will add a couple of global variables to allow us to track what calculations are required. We will be allowing the user to specify a value for one of the three dimensions in the list – length, perimiter or area – and then updating the other two values accordingly. To this end, we will use the calc_current_dimension to remember what type of value was most recently supplied, and hold the value itself in calc_current_dimension_size. The changes can be seen in Listing 27.2.

/* Constants. */

#define CALC_BUFFER_LENGTH 32
#define CALC_FORMAT_LENGTH 8
#define CALC_PI 3.14159265358979323846

/* 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 enum calc_dimension calc_current_dimension = CALC_DIMENSION_LENGTH;

static double calc_current_dimension_size = 10.0;

static char calc_buffer[CALC_BUFFER_LENGTH];

static char calc_format[CALC_FORMAT_LENGTH];

static char *calc_not_applicable = "n/a";

Listing 27.2: The constants and global variables used for calculations.

To allow a dimension to be set, we will supply a new calc_set_dimension() function, which takes the type of dimension and the new value. This will allow us to supply values read from the fields in the window.

/* Set one of the dimensions. */

void calc_set_dimension(enum calc_dimension dimension, double value)
{
        if (dimension == CALC_DIMENSION_NONE)
                return;

        calc_current_dimension = dimension;
        calc_current_dimension_size = value;
}

Finally, we will add three new functions – calc_get_length(), calc_get_perimeter() and calc_get_area() – which will operate in a similar way to the existing calc_get_sides() and calc_get_internal_angle() functions. When called, they will calculate the necessary value and write it to a string buffer, before returning a pointer to that buffer.

The additional code, including a couple of internal functions to convert to and from area, can be seen in Listing 27.3. This is somewhat incidental to the purpose of this tutorial, so there should be no reason not to take it as a ‘black box’ if you do not wish to get bogged down in the mathematics.

/* The length of a side, or diameter of a circle. */

char *calc_get_length(void)
{
        double length = 0.0;

        switch (calc_current_dimension) {
        case CALC_DIMENSION_LENGTH:
        case CALC_DIMENSION_DIAMETER:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, calc_current_dimension_size);
                break;

        case CALC_DIMENSION_PERIMETER:
                if (calc_current_sides > 1)
                        length = calc_current_dimension_size / calc_current_sides;
                else
                        length = calc_current_dimension_size / CALC_PI;
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, length);
                break;

        case CALC_DIMENSION_AREA:
                length = calc_length_from_area();
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, length);
                break;

        default:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_not_applicable);
                break;
        }

        return calc_buffer;
}

/* The perimeter, or circumference of a circle. */

char *calc_get_perimeter(void)
{
        double perimeter = 0.0;

        switch (calc_current_dimension) {
        case CALC_DIMENSION_LENGTH:
        case CALC_DIMENSION_DIAMETER:
                if (calc_current_sides > 1)
                        perimeter = calc_current_dimension_size * calc_current_sides;
                else
                        perimeter = calc_current_dimension_size * CALC_PI;
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, perimeter);
                break;

        case CALC_DIMENSION_PERIMETER:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, calc_current_dimension_size);
                break;

        case CALC_DIMENSION_AREA:
                if (calc_current_sides > 1)
                        perimeter = calc_length_from_area() * calc_current_sides;
                else
                        perimeter = calc_length_from_area() * CALC_PI;
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, perimeter);
                break;

        default:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_not_applicable);
                break;
        }

        return calc_buffer;
}

/* The area. */

char *calc_get_area(void)
{
        double area = 0.0;

        switch (calc_current_dimension) {
        case CALC_DIMENSION_LENGTH:
        case CALC_DIMENSION_DIAMETER:
        case CALC_DIMENSION_PERIMETER:
                area = calc_area_from_length();
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, area);
                break;

        case CALC_DIMENSION_AREA:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_format, calc_current_dimension_size);
                break;

        default:
                string_printf(calc_buffer, CALC_BUFFER_LENGTH, calc_not_applicable);
                break;
        }

        return calc_buffer;
}

/* If the current dimension is Area, return the length. */

static double calc_length_from_area(void)
{
        if (calc_current_dimension != CALC_DIMENSION_AREA)
                return 0.0;

        switch (calc_current_sides) {
        case 1:
                return sqrt(4.0 * calc_current_dimension_size / CALC_PI);
        case 3:
                return sqrt(4.0 * calc_current_dimension_size / sqrt(3.0));
        case 4:
                return sqrt(calc_current_dimension_size);
        default:
                return 0.0;
        }
}

/* If the current dimension is length or perimeter, return the area. */

static double calc_area_from_length(void)
{
        double length = 0.0;

        switch (calc_current_dimension) {
        case CALC_DIMENSION_LENGTH:
        case CALC_DIMENSION_DIAMETER:
                length = calc_current_dimension_size;
                break;

        case CALC_DIMENSION_PERIMETER:
                if (calc_current_sides > 1)
                        length = calc_current_dimension_size / calc_current_sides;
                else
                        length = calc_current_dimension_size / CALC_PI;
                break;

        default:
                return 0.0;
        }

        switch (calc_current_sides) {
        case 1:
                return CALC_PI * length * length / 4.0;
        case 3:
                return sqrt(3.0) * length * length / 4.0;
        case 4:
                return length * length;
        default:
                return 0.0;
        }
}

Listing 27.3: The additional calculation routines

Updating the display

To make use of the icons and code that we have added so far, we will also need to update c.win. To start with, we need to add constants for the new writable icons that have been added to the window template, as shown in Listing 27.4. The numbers must match with those defined in the templates file.

/* Main Window Icons. */

#define WIN_ICON_SHAPE 0
#define WIN_ICON_SHAPE_POPUP 1
#define WIN_ICON_SHAPE_FIELD 2
#define WIN_ICON_SIDES_FIELD 4
#define WIN_ICON_INT_ANGLE_FIELD 6
#define WIN_ICON_LENGTH_FIELD 8
#define WIN_ICON_PERIMETER_FIELD 10
#define WIN_ICON_AREA_FIELD 12

Listing 27.4: Constants for the handles of the new icons

We can now extend the win_update_all_calculations() function to fill in the new writable icons, using the functions added to c.calc. The code is in Listing 27.5, but in addition to the three extra copy-and-redraw blocks which should already be familiar, there is some additional code added at the end.

static void win_update_all_calculations(void)
{
        char            *text = NULL;
        int             index;
        wimp_caret      caret;
        wimp_icon_state state;

        /* Update the window data. */

        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);
        }

        text = calc_get_length();
        if (text != NULL) {
                icons_strncpy(win_handle, WIN_ICON_LENGTH_FIELD, text);
                wimp_set_icon_state(win_handle, WIN_ICON_LENGTH_FIELD, 0, 0);
        }

        text = calc_get_perimeter();
        if (text != NULL) {
                icons_strncpy(win_handle, WIN_ICON_PERIMETER_FIELD, text);
                wimp_set_icon_state(win_handle, WIN_ICON_PERIMETER_FIELD, 0, 0);
        }

        text = calc_get_area();
        if (text != NULL) {
                icons_strncpy(win_handle, WIN_ICON_AREA_FIELD, text);
                wimp_set_icon_state(win_handle, WIN_ICON_AREA_FIELD, 0, 0);
        }

        /* Correct the caret position if required. */

        if (xwimp_get_caret_position(&caret) != NULL)
                return;

        if (caret.w != win_handle)
                return;

        state.w = caret.w;
        state.i = caret.i;
        if (xwimp_get_icon_state(&state) != NULL)
                return;

        index = string_ctrl_strlen(state.icon.data.indirected_text.text);

        if (caret.index < index)
                index = caret.index;

        xwimp_set_caret_position(caret.w, caret.i, 0, 0, -1, index);
}

Listing 27.5: The code to update all of the icon contents

The additional code checks to see whether the caret is in our window, because if it is, we need to take care when changing the text in the writable icons. Although the Wimp does a lot of the caret handling for us, one thing that it does not do is update the position of the caret if we change the text of the buffer containing it. Generally this will lead to the effect seen on the left of Figure 27.7, where the caret ends up on top of one of the characters in the new value.

This is fairly benign, since any attempt to type into the icon or move the caret will realign it as expected – although the keypress might not go exactly where the user expects! More dangerous is the effect shown on the right of Figure 27.7, which can happen if the caret was towards the end of the original icon text and the new text isn’t long enough to reach it. Here, the caret ends up beyond the terminator of the new string, and pressing Backspace can actually delete the terminator with potentially fatal consequences for the system.

Figure 27.7: The caret misaligned with the text on the left, or beyond the terminator on the right

It is a little surprising that the Wimp does’t catch these problems for us. Since it doesn’t, however, any application which changes the text in a writable icon’s buffer must then check to see if the caret is in the icon and, if it is, reposition it correctly within the new string. Often this will be done as part of a general update routine for a dialogue, at the same time as moving the caret out of any icons which have become shaded, and like most libraries, SFLib offers a number of functions to help us. However, we have chosen to implement our own solution here – not least because it allows us to introduce a couple of new SWIs.

Finding and moving the caret

The Wimp provides two SWIs to track and move the caret, which are predictably called Wimp_GetCaretPosition and Wimp_SetCaretPosition. OSLib defines two functions to access them, which cover the usage available on all versions of the Wimp since RISC OS 3.1; RISC OS Select added some additional options, but since these have never made it into any other versions of the OS, they are of limited use.

Wimp_GetCaretPosition takes a pointer to a block of memory, and fills it with details of the current position of the caret; OSLib defines a pair of functions as follows:

extern void wimp_get_caret_position(
        wimp_caret *caret
);

extern os_error *xwimp_get_caret_position(
        wimp_caret *caret
);

The only parameter is a pointer to a wimp_caret structure, which is defined like this:

struct wimp_caret {
        wimp_w          w;
        wimp_i          i;
        os_coord        pos;
        int             height;
        int             index;
};

typedef struct wimp_caret wimp_caret;

On exit, w contains the handle of the window containing the caret (that is, the one with input focus). This could be any window, belonging to any application – there is no guarantee that it is one belonging to our application. A window handle of −1 (or wimp_BACKGROUND) indicates that there is no caret – nothing has input focus.

If there is a caret, then pos gives its x and y coordinates in OS units, relative to the work area origin of the window that it is in. To find the position on screen relative to the screen origin, it would be necessary to do a similar calculation to the one that we used in Section 24.3 when finding the position of a pop-up menu icon.

If the caret is in a writable icon, the icon handle is stored in i; otherwise this contains −1 (or wimp_ICON_WINDOW). If it is in an icon, index contains the position in the string, starting from zero before the first character.

The final piece of information is the caret height, in OS units, which is contained in height. This is actually a bit-field as shown in Table 27.1 – fortunately it isn’t a value that we need to make use of for now.

Bit(s)Meaning
0–15The caret height in OS units (0 to 65535)
16–23The caret colour, if bit 26 is set (0 to 255)
24Use a VDU 5 caret, instead of an anti-aliased one
25The caret is invisible
26Use the colour in bits 16–23 instead of Wimp colour 11
27The colour in bits 16–23 is untranslated, instead of a Wimp colour
30The caret is a ghost caret (RISC OS 5.27+ only)

Table 27.1: The contents of the height field in wimp_caret

Our code calls Wimp_GetCaretPosition and checks the returned wimp_w handle to see if it matches the handle of our window – which is stored in win_handle. If it does, then we set up a wimp_icon_state structure with the wimp_w and wimp_i handles returned by Wimp_GetCaretPosition in order to find the address of the icon’s text buffer.

The length of the contents of the icon buffer is read back using SFLib’s string_ctrl_strlen() – this behaves like the standard strlen(), but will accept any control character (ie. any character whose ASCII value is less than 32) as a terminator. The Wimp can be a little vague about string terminators and some template editors use values like '\r' instead of '\0', so it can be wise to be flexible.

Once we know the length of the current icon text, we can use this to apply an upper bound to the index read from Wimp_GetCaretPosition. Now we are ready to use Wimp_SetCaretPosition to set the position of the caret; OSLib defines this as follows:

extern void wimp_set_caret_position(
        wimp_w w,
        wimp_i i,
        int x,
        int y,
        int height,
        int index
);

extern os_error *xwimp_set_caret_position(
        wimp_w w,
        wimp_i i,
        int x,
        int y,
        int height,
        int index
);

Whilst we can use Wimp_SetCaretPosition to set the caret to any location on screen that we desire, when working with writable icons we only need to specify w, i and index. So long as we also set height to −1, the Wimp will work the rest our for us.

With this code in place, the caret will be left in its current position within an icon when the contents changes, if that is possible. If it would fall off the end of the new text, however, then it will be placed at the end – safely before the string terminator!

When compiled and run, our application should look similar to Figure 27.8. The fields will update when the shape is changed, but typing in new values will have no effect – something that we will need to rectify in the next chapter.

Figure 27.8: Our application will now fill in the calculated fields in its window

The full code can be found in Download 27.1.

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

Moving between icons

One thing that our application can do, without any effort from us, is move the caret between writable icons when the Up and Down cursor keys or the Tab key is used. Asuming that the icon numbers are in the order specified in Section 27.2, and the ESGs are all the same, then the Down or Tab keys should move to the next field, while Up and Shift-Tab will move to the previous one.

This functionality is provided for us by the Wimp, and is a result of the K validation command that we glossed over earlier. It controls how the Wimp handles keypresses in a writable icon and takes the option flags listed in Table 27.2, which can be applied in any combination.

OptionAction
AThe Down and Up cursor keys will move the caret to the numerically next and previous writable icons in the same ESG, wrapping around at the ends of the list.
TThe Tab and Shift-Tab keys will move the caret to the numerically next and previous writable icons in the same ESG, wrapping around at the ends of the list.
RThe Return key will move the caret to the numerically next writable icon in the same ESG; if at the end of the list, a wimp_KEY_RETURN code will be passed to the application.
DIn addition to performing the standard actions in the icon, all Copy, Delete, Shift-Copy, Ctrl-U and Ctrl-Copy keypresses will be passed to the application.
NAll keypresses in the icon will be passed to the application, even if the Wimp has acted on them first.
CDo not perform selection or clipboard operations on the icon and instead pass associated keypresses to the application, where the Wimp includes clipboard support.

Table 27.2: The option flags for the K validation command

Our application set its icons to use Kta, which means that the cursor keys and Tab key will navigate around the fields while Return will always be passed to us to handle. This is what the Style Guide requires applications to do, and it makes sense to follow the behaviour: it can be surprisingly disconcerting as a user to find different behaviour in a dialogue box, especially if it results in changes being lost or applied unexpectedly!

The D and N flags are useful when an application needs to know about keypresses that the Wimp has handled. This is relatively rare, as it can result in unexpected behaviour for the user and there are few occasions where the extra effort is justified, but we will look at an application later on. At present, we have no way to receive key codes which are passed to our application, but we will rectify that in the next chapter.

The C flag will allow an application to prevent an icon supporting the global clipboard, on versions of the Wimp which implement it (RISC OS Select and RISC OS 5.27 onwards). This is another option which is unlikely to have much purpose in ‘normal’ use, since applications shouldn’t be disabling functionality like text selection unless they have a very good reason for doing so. It isn’t required to protect passwords, incidentally, since the D validation command (see Chapter 15) will prevent an icon’s contents from being cut or copied; whilst still allowing text to be pasted in from a password manager would still be useful.