Chapter 2: A Side Toolbox

If one were to survey members of the RISC OS community to find out what a “toolbox” looked like, the answer which would come back would probably be along the lines of “like the one in Draw”. Draw’s toolbox has been present as standard on all systems since RISC OS 2, and is still functional to this day. It’s also one of the easiest panes to implement, so let’s start by making one of our own.

Figure 2.1: Draw’s toolbar has become someting of a design classic

Making our first pane

The first thing that we will need to do is design the pane itself. As far as the Wimp is concerned, panes are just normal windows, so we’ll go back to our template editor of choice (again, I’m using WinEd here, but other editors would also be fine) and add a new window called “Toolbox”. We can see that window in the browser in Figure 2.2, although the screenshot shows it after editing is complete – WinEd (and probably most other template editors) will give it a full set of scroll bars in the template browser by default.

Figure 2.2: The updated templates file, with the new toolbox pane

Opening the window template up for editing, we can change the window title to something short like “TB” – although this doesn’t really matter, since the title will never be shown to the user. To make it look like a toolbox, we’ve also added four action buttons which are each 92 × 92 OS Units and offset by 4 OS Units from each edge of the window: the visible area is then adjusted to be 100 × 388 OS Units. The end result can be seen in Figure 2.3; the buttons are just for show, as we won’t be using them for anything in this demonstration.

Figure 2.3: Editing the toolbox pane in WinEd

Next,'' we must adjust the window’s flags and parameters to meet the requirements of a toolbox pane, as seen in Figure 2.4. This is the Edit window dialogue in WinEd; other template editors will have a close equivalent.

Our first action is to turn off all of the window furniture – including the title bar. The pane won’t be able to move independently of the window that it’s attached to, so there’s no need to give the user the option of trying! Under the heading of Window behaviour, the Moveable flag is also unset – this corresponds to the ‘window is moveable’ flag in bit 1 of the window flags, and indicates that the user can’t initiate window movement – the application still can, though.

Figure 2.4: Editing the pane’s window flags in WinEd

The other flag which we have set is Pane, which corresponds to the ‘window is a pane’ flag at bit 5 of the window flags. The Programmer’s Reference Manual is a little vague about what this flag does, but most developers will probably have been disappointed to discover that what it doesn’t do is cause the window follow its parent around the screen automatically! In fact it does a few important things, which we’ll discuss later in this chapter.

Down at the bottom of the dialogue, all of the scroll events are off, since we don’t need the pane to scroll. In addition, all of the window colours have been left at their default, Style Guide compliant values.

With the toolbox window design added to the templates and the file saved back into our application, we can add a couple of lines to PROCinitialise to load it and create a window for us to work with, as shown in Listing 2.1.

PROCtemplate_load("ToolBox", b%, buffer_size%, -1)
SYS "Wimp_CreateWindow",,b% TO ToolBoxWindow%

Listing 2.1: Loading the toolbox template

This follows the same pattern as used for the other two templates: creating the window, and placing the resulting handle into the ToolBoxWindow% variable for future reference.

Handling pane events

Handling panes is actually fairly straight-forward, although the details can seem confusing at first sight! It boils down to this: if the Wimp won’t do the work for us, we need to track movement of the main window and ensure that every time it moves, we move the pane into the correct relative position at the same time. There are only two ways that a window can move: if the application does it, or if it is done by something outside the application (such as the user, or another application). In the latter case, the request will arrive through the Open_Window_Request event and we’ll know about it; in the former we’ll have initiated the action, so we’ll also know about it.

In our existing application, we have the following two lines in PROCpoll to handle the Open_Window_Request and Close_Window_Request events:

WHEN 2      : SYS "Wimp_OpenWindow",,b%
WHEN 3      : SYS "Wimp_CloseWindow",,b%

These just pass the requests straight back to the Wimp to handle, which is all that’s necessary for a simple application. If we want to adjust the position of the toolbox pane whenever the main window moves, however, then we will need to do a bit more work. To keep PROCpoll tidy, we will define two new event handling procedures – PROCopen_window_request and PROCclose_window_request – and use these to replace the calls to Wimp_OpenWindow and Wimp_CloseWindow, as seen in Listing 2.2.

DEF PROCpoll
LOCAL reason%

SYS "Wimp_Poll", &3C01, b% TO reason%

CASE reason% OF
  WHEN 2      : PROCopen_window_request(b%)
  WHEN 3      : PROCclose_window_request(b%)
  WHEN 6      : PROCmouse_click(b%)
  WHEN 9      : PROCmenu_selection(b%)
  WHEN 17, 18 : IF b%!16 = 0 THEN Quit% = TRUE
ENDCASE
ENDPROC

Listing 2.2: Handling events for the toolbox pane

Before either of these two new procedures will do anything, they will check the handle of the window to which the event applies. If it’s the handle of the main window, then they will need to do the work required to handle the pane as well; for any other window, they can simply call the appropriate Wimp SWI as before.

In the case of PROCopen_window_request, we will change the code so that any Open_Window_Request events relating to the main window are passed on once again to another new procedure: PROChandle_pane_windows. Events for other windows will simply be passed on to the Wimp_OpenWindow SWI. This code to do this can be seen in Listing 2.3; We’ll worry about what PROChandle_pane_windows looks like later on.

DEF PROCopen_window_request(b%)
IF !b% = MainWindow% THEN
  PROChandle_pane_windows(b%)
ELSE
  SYS "Wimp_OpenWindow",,b%
ENDIF
ENDPROC

Listing 2.3: Dealing with Open Window Request events

Of course, if the main window and its toolbox should be acting as one then, in addition to moving when the main window moves, the pane should be closed when the main window closes. We can look after this in a very similar way with our new PROCclose_window_request procedure, seen in Listing 2.4. If the window being closed is the main window, then the toolbox window will be closed first; either way, the window referenced by the event is closed afterwards.

DEF PROCclose_window_request(b%)
IF !b% = MainWindow% THEN
  !q% = ToolBoxWindow%
  SYS "Wimp_CloseWindow",,q%
ENDIF

SYS "Wimp_CloseWindow",,b%
ENDPROC

Listing 2.4: Dealing with Close Window Request events

These changes deal with things outside of our application moving the main window, but what about when we change its position ourselves? Fortunately, there’s only one place where we do this, which is at the end of PROCopen_main_window.

REM Open the window at the top of the window stack.

q%!28 = -1 : REM Window to open behind (-1 is top of stack)
SYS "Wimp_OpenWindow",,q%

This is just another call to Wimp_OpenWindow, so we can simply replace it with a call to PROChandle_pane_windows in the same way as we have done in the Open_Window_Request event handler. This means that our new PROCopen_main_window will look as seen in Listing 2.5.

DEF PROCopen_main_window
LOCAL screen_width%, screen_height%, window_size%

REM Get the window details.

!q% = MainWindow%
SYS "Wimp_GetWindowState",,q%

REM If the window isn't open, resize and centre it on the screen.

IF (q%!32 AND &10000) = 0 THEN
  window_size% = WindowSize%

  REM Read the screen dimensions.

  screen_width% = FNread_mode_dimension(11, 4)
  screen_height% = FNread_mode_dimension(12, 5)

  REM Ensure that the window fills no more than 75% of either dimension.

  IF window_size% > (screen_width% * 0.75) THEN window_size% = screen_width% * 0.75
  IF window_size% > (screen_height% * 0.75) THEN window_size% = screen_height% * 0.75

  REM Update the window dimensions.

  q%!4 = (screen_width% - window_width%) / 2    : REM Visible Area X0
  q%!8 = (screen_height% - window_height%) / 2  : REM Visible Area Y0

  q%!12 = q%!4 + window_width%                  : REM Visible Area X1
  q%!16 = q%!8 + window_height%                 : REM Visible Area Y1

  REM Reset the scroll offsets.

  q%!20 = 0                                     : REM X Scroll Offset
  q%!24 = 0                                     : REM Y Scroll Offset
ENDIF

REM Open the window at the top of the window stack.

q%!28 = -1 : REM Window to open behind (-1 is top of stack)
PROChandle_pane_windows(q%)
ENDPROC

Listing 2.5: Opening the main window and its toolbox pane

Aside from working out what goes into the PROChandle_pane_windows procedure, that’s all of the code that we will need in order to handle our new toolbox.

Setting the visible area

Now that we have ensured that our PROChandle_pane_windows procedure will be called every time the positions of the main window and toolbox pane need to be updated, all that remains is to work out how to join the two windows together on screen.

The procedure takes a single parameter, main%, which is a pointer to the Wimp_WindowState block for the main window. Whether this arrived through Wimp_Poll into the block pointed to by b%, or was put into the block pointed to by q% using Wimp_WindowState in PROCopen_main_window, it contains details of where the main window is required to be opened.

The first thing that we need to do is to create an equivalent window state block for the toolbox pane, which we can then update with the necessary values calculated from the main window’s position. Unfortunately we can’t use our usual scratch block of memory pointed to by q%, because that could already be holding the data for the main window if we have been called from PROCopen_main_window. However, we know that only 36 bytes will be in use from what is at least a 256 byte block, so we can just load the pane information into some of the unused space at the end, with an offset of 64 bytes. To make things simpler to follow, we’ll define a local variable toolbox% which points to this offset, then access the toolbox pane’s data through it.

toolbox% = main% + 64

!toolbox% = ToolBoxWindow%
SYS "Wimp_GetWindowState",,toolbox%

We will start by positioning the toolbox in the X and Y dimensions by adjusting its visible area, so that it aligns with the main window. It will be useful later on for this code to be in a procedure of its own, so we will create one called PROCposition_side_toolbox and call it as follows.

PROCposition_side_toolbox(main%, toolbox%)

It will take two parameters: a pointer to the main window state block as parent%, and a pointer to the toolbox pane’s window state block as toolbox%. We will pass main% and toolbox% to these respectively.

Since the toolbox can’t be resized by the user, we can be fairly confident that its width and height will still be as they were saved in the templates file. These can be used to give us the values for the toolbox pane’s width and height, saving us from having to use constants in the code. Before making any changes to the toolbox’s window state block’s contents, we therefore calculate the width and height of the pane’s visible area and store the two values for future use.

width% = toolbox%!12 - toolbox%!4         : REM Visible Area X1 - X0
height% = toolbox%!16 - toolbox%!8        : REM Visible Area Y1 - Y0

As already noted, we know where the main window should be on screen from its visible area. We also know the relationship that we want to have between the visible area of the main window and that of its pane, as shown in Figure 2.5. We can therefore update the pane’s window state block so that its visible area is in the correct position relative to the main window, then call Wimp_OpenWindow for both windows in turn.

Figure 2.5: The relationship between the main window and its toolbox

Figure 2.5 shows the relationship between the visible areas of the main window and its pane. The tops of the two windows (Y1 for the two visible areas) should be level, so we can copy the Y1 value from the main window to the pane. The bottom of the pane (its Y0) is the height of the pane below the top of the main window.

toolbox%!16 = parent%!16                  : REM Visible Area Y1
toolbox%!8 = parent%!16 - height%         : REM Visible Area Y0

In a similar way, the right-hand side of the pane (its X1) is on the left-hand side of the main window, while its left-hand side is the width of the pane further to the left.

toolbox%!12 = parent%!4                   : REM Visible Area X1
toolbox%!4 = parent%!4 - width%           : REM Visible Area X0

Putting this all together results in PROCposition_side_toolbox as seen in Listing 2.6.

DEF PROCposition_side_toolbox(parent%, toolbox%)
LOCAL width%, height%

REM Find the width and height of the toolbox pane's visible area.

width% = toolbox%!12 - toolbox%!4         : REM Visible Area X1 - X0
height% = toolbox%!16 - toolbox%!8        : REM Visible Area Y1 - Y0

REM Move the toolbox pane so that it's in the correct X and Y position
REM relative to where the parent window is to go.

toolbox%!4 = parent%!4 - width%           : REM Visible Area X0
toolbox%!8 = parent%!16 - height%         : REM Visible Area Y0
toolbox%!12 = parent%!4                   : REM Visible Area X1
toolbox%!16 = parent%!16                  : REM Visible Area Y1
ENDPROC

Listing 2.6: The code to position the toolbox horizontally and vertically

Lost in the stack

The code above deals with the position of the pane in the X and Y dimensions by adjusting its visible area, but what about the Z dimension – its position in the window stack? The Wimp requires that panes appear directly above their parent window; however, this is still left up to the application to manage.

There’s a very practical reason for this stacking requirement. If something like a toolbar pane goes behind its parent window in the stack, then it will very likely be obscured by its parent. Even if the pane is in front then, unless it is immediately in front, it’s possible for other windows to get in between as seen in Figure 2.6 (the application was deliberately broken in order to allow this to occur).

Figure 2.6: If we’re not careful, other windows can get in the way

There are a couple of more technical reasons, too, which relate to the behaviour of the ‘window is a pane’ flag. When checking to see if our main window is at the top of the window stack, or obscured by any other windows (for the ‘window is fully visible’ flag at bit 17 of the window flags), the Wimp will ignore any windows immediately in front of it so long as they have their pane flags set. Without use of the pane flag, a window whose pane obscures any part of its work area can never be reported as being fully visible by the Wimp.

In a similar way, if a window with its pane flag set gains the caret, then the Wimp will give input focus to (and set the ‘window has input focus’ flag at bit 20 of the window flags for) the first window below it in the stack which is not a pane. Finally, when working out which bits of a window might need redrawing following a re-size, the Wimp will treat any panes immediately above it as being transparent – on the basis that the application may be about to move them anyway.

Keeping the Z order of the windows correct is fairly straight-forward. The window state block for the main window contains the position in the window stack at which it should be opened, in terms of the handle of the window that it is positioned beneath at offset 28. Since the toolbox pane should be in front of the main window, then returning to PROChandle_pane_windows following the call to PROCposition_side_toolbox, we can copy this value into the pane’s block so that the pane appears at the correct position in the stack.

IF main%!28 <> ToolBoxWindow% THEN toolbox%!28 = main%!28

There’s one exception to this, however. If the Wimp is already asking us to open the main window behind the pane, then both the pane and the main window must already be in the correct positions in the window stack. In this case, there’s no point attempting to open the pane behind itself, so we just leave things as they are. The pane is now ready to be opened in its new location, using a call to Wimp_OpenWindow.

SYS "Wimp_OpenWindow",,toolbox%

With the pane open in the correct place, all that remains to do is to open the main window. This is done using the block from the Open_Window_Request event almost unchanged: the only thing that we alter is the handle of the window to open behind, which we set to the handle of the pane. This ensures that the pane is always directly in front of the main window.

main%!28 = ToolBoxWindow%

SYS "Wimp_OpenWindow",,main%

Putting all of the code above together, PROChandle_pane_windows will look as shown in Listing 2.7.

DEF PROChandle_pane_windows(main%)
LOCAL toolbox%

REM Get the Window State block for the toolbox pane, using some of the
REM spare space above the data for the state of the main window.
REM
REM Note: ON RISC OS 5, we could more clearly use DIM toolbox% LOCAL 64
REM here to allocate the required memory from the stack.

toolbox% = main% + 64

!toolbox% = ToolBoxWindow%
SYS "Wimp_GetWindowState",,toolbox%

REM Move the toolbox pane so that it's in the correct X and Y position
REM relative to where the main window is to go.

PROCposition_side_toolbox(main%, toolbox%)

REM Unless the main window is to be opened behind the toolbox pane, meaning
REM that the pane must already be in the correct place in the stack, set the
REM pane's Open Behind so that it appears in the stack where the main window
REM is required to go. Then (re-)open the toolbox.

IF main%!28 <> ToolBoxWindow% THEN toolbox%!28 = main%!28

SYS "Wimp_OpenWindow",,toolbox%

REM Set the main window's Open Behind so that it opens behind the pane.

main%!28 = ToolBoxWindow%

REM (Re-)open the main window.

SYS "Wimp_OpenWindow",,main%
ENDPROC

Listing 2.7: The code to position the pane and the main window

Running our new application should reveal a main window similar to that shown in Figure 2.7. If the main window is moved, the pane should remain securely attached at all times.

Figure 2.7: Our main window with a simple vertical toolbox attached

The full code can be found in Download 2.1.

Download 2.1
The source code and files in this example are made available under the MIT No Attribution License.

Unexpected movement

There is one small problem with our application as it stands, though. In order to get the positions in the window stack correct we open the toolbox first, based on the position that we would be requesting that the Wimp would open the main window, then open the main window itself. However, if it isn’t possible to open the window where we request, then the Wimp may open the window in a slightly different location. If this happens, it will result in the toolbox becoming ‘detached’ from the main window.

The solution is to check the position of the main window after the call to Wimp_OpenWindow and, if necessary, re-open the toolbox pane in the correct location. We could find this information out by calling Wimp_GetWindowState for the main window, but in practice there’s no need because the information is already waiting for us.

Since the arrival of the Nested Window Manager (or Nested Wimp) with RISC OS 3.8, the Wimp_OpenWindow SWI has been documented as returning with the contents of the window state block, passed to it in R1, updated to reflect where the window was actually opened. In the Nested Window Manager Functional Specification, Acorn wrote that “since RISC OS 2 (and possibly even earlier), Wimp_OpenWindow has had undocumented return conditions: values at R1+4 to R1+24 are updated to represent the actual parameters of the opened window after valid ranges have been taken into account, and the window has been forced on-screen (if applicable).”

Thanks to this piece of retrospective documentation, it is safe for us to rely on this behaviour in new software. As a result, following the call to Wimp_OpenWindow, the block pointed to by main% will contain the positions at which the window was actually opened on screen. In order to make use of this, we will start by taking copies of the visible area dimensions which relate to the toolbox before the call to Wimp_OpenWindow.

left% = main%!4
top% = main%!16

SYS "Wimp_OpenWindow",,main%

Using this information, we can check the visible area returned from the call to Wimp_OpenWindow and see whether any of the positions which would affect the toolbox have changed. If they have, we can re-position the toolbox in the correct place, then call Wimp_OpenWindow again to re-open it.

IF (main%!4 <> left%) OR (main%!16 <> top%) THEN
  PROCposition_side_toolbox(main%, toolbox%)
  SYS "Wimp_OpenWindow",,toolbox%
ENDIF

The toolbox is anchored to the top and left sides of the visible area, so there’s no need to check the right or bottom sides. Adding these changes to PROChandle_pane_windows results in the code in Listing 2.8.

DEF PROChandle_pane_windows(main%)
LOCAL toolbox%, top%, left%

REM Get the Window State block for the toolbox pane, using some of the
REM spare space above the data for the state of the main window.
REM
REM Note: ON RISC OS 5, we could more clearly use DIM toolbox% LOCAL 64
REM here to allocate the required memory from the stack.

toolbox% = main% + 64

!toolbox% = ToolBoxWindow%
SYS "Wimp_GetWindowState",,toolbox%

REM Move the toolbox pane so that it's in the correct X and Y position
REM relative to where the main window is to go.

PROCposition_side_toolbox(main%, toolbox%)

REM Unless the main window is to be opened behind the toolbox pane, meaning
REM that the pane must already be in the correct place in the stack, set the
REM pane's Open Behind so that it appears in the stack where the main window
REM is required to go. Then (re-)open the toolbox.

IF main%!28 <> ToolBoxWindow% THEN toolbox%!28 = main%!28

SYS "Wimp_OpenWindow",,toolbox%

REM Set the main window's Open Behind so that it opens behind the pane.

main%!28 = ToolBoxWindow%

REM (Re-)open the main window.

left% = main%!4
top% = main%!16

SYS "Wimp_OpenWindow",,main%

REM If the main window was moved on opening (perhaps if it opened off-screen),
REM re-position the toolbox against the new position and re-open it.

IF (main%!4 <> left%) OR (main%!16 <> top%) THEN
  PROCposition_side_toolbox(main%, toolbox%)
  SYS "Wimp_OpenWindow",,toolbox%
ENDIF
ENDPROC

Listing 2.8: Spotting and handling changes to the main window position

The full code can be found in Download 2.2.

Download 2.2
The source code and files in this example are made available under the MIT No Attribution License.

If you’re looking at this code and wondering if we could avoid having to open the toolbox pane twice by doing things in a different order, the answer is that we can – but not in all situations. We’ll stick to this universal approach for now, but will return to the subject in Chapter 5 in order to investigate a possible optimisation and examine why it can’t always be used!

A small improvement

If you’re familiar with Draw, then there’s one small difference which you might notice in the behaviour of the toolbox in our example application compared to the toolbox in Draw. If a Draw window is moved off the left-hand edge of the screen, then the toolbox pane stops moving when it hits the edge and begins to retract over the parent window as seen in Figure 2.8. Only when the pane is flush with the side of the main window does it begin to move off the screen again.

Figure 2.8: Draw’s toolbox stays on screen as long as possible

Since we have complete control over the positioning of the toolbox pane, this is easy to implement in our own application. The changes needed are all in PROCposition_side_toolbox, where we set the coordinates of the pane’s visible area. In the code above, we used the following fixed calculations:

REM Move the pane so that it's in the correct X and Y position
REM relative to where the main window is to go.

toolbox%!4 = parent%!4 - width%           : REM Visible Area X0
toolbox%!8 = parent%!16 - height%         : REM Visible Area Y0
toolbox%!12 = parent%!4                   : REM Visible Area X1
toolbox%!16 = parent%!16                  : REM Visible Area Y1

We can replace this by testing the minimum X0 coordinate of the main window’s visible area, then applying different calculations for the pane’s X coordinates as follows:

The code to implement this can be seen in Listing 2.9.

REM Move the pane so that it's in the correct X and Y position
REM relative to where the main window is to go.

CASE TRUE OF
WHEN parent%!4 > width%
        toolbox%!4 = parent%!4 - width%         : REM Visible Area X0
        toolbox%!12 = parent%!4                 : REM Visible Area X1
WHEN parent%!4 > 0
        toolbox%!4 = 0                          : REM Visible Area X0
        toolbox%!12 = width%                    : REM Visible Area X1
OTHERWISE
        toolbox%!4 = parent%!4                  : REM Visible Area X0
        toolbox%!12 = parent%!4 + width%        : REM Visible Area X1
ENDCASE

toolbox%!8 = parent%!16 - height%         : REM Visible Area Y0
toolbox%!16 = parent%!16                  : REM Visible Area Y1

Listing 2.9: Calculating the horizontal position of the pane more flexibly

With this in place, our own pane begins to behave in a more Draw-like way when it reaches the edge of the screen, as seen in Figure 2.9.

Figure 2.9: Our own toolbox can now behave more dynamically, too

The full code can be found in Download 2.3.

Download 2.3
The source code and files in this example are made available under the MIT No Attribution License.

We’ve now seen all of the building blocks required to attach a pane to our window, but there’s no need to have it on the left-hand side of the work area – or even limit ourselves to a single pane! In the next chapter, we’ll take a look at adding a second pane in an alternative toolbar configuration.