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.
The 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: it’s just the details which 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 also move the pane into the correct relative position. Fortunately, there are only two ways that a window can move: if we do it ourselves, or if it is done by something outside our application (such as the user, or another application). In the latter case, the request will arrive through the Open_Window_Request event; in the former we’ll have initiated the action, so we’ll 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
Each of these event-handling procedures will need to check each incoming event, to see which window it applies to. If it’s the handle of the main window, then – in addition to calling the appropriate Wimp SWI as before – any additional work required to handle the pane will need to be performed as well.
In the case of PROCopen_window_request(), we will change the code so that for any Open_Window_Request events relating to the main window, a new PROChandle_pane_windows() procedure will be called after the event has been passed to the Wimp_OpenWindow SWI. This code to do this can be seen in Listing 2.3; PROChandle_pane_windows() will look after moving the toolbox pane for us, but we’ll worry about what it looks like later on.
DEF PROCopen_window_request(b%) SYS "Wimp_OpenWindow",,b% IF !b% = MainWindow% THEN PROChandle_pane_windows(b%) 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 follow it with a call to PROChandle_pane_windows() in the same was 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) SYS "Wimp_OpenWindow",,q% 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 new PROChandle_pane_windows() procedure will be called following every Wimp_OpenWindow call which applies to the main window, all that remains to be done is to work out how to adjust the toolbox pane so that the two windows appear to be joined together on screen. To do this, we will need to know where the main window ended up on screen once it was opened, and fortunately this information is already waiting for us.
Since the arrival of the Nested Wimp with RISC OS 4.02, 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. The PROChandle_pane_windows() procedure takes a single parameter, main%, which is a pointer to the block that was used to open the main window. Whether this arrived through Wimp_Poll into the block pointed to by b%, or was set up in the block pointed to by q% by a call to Wimp_WindowState in PROCopen_main_window, it has been passed to Wimp_OpenWindow and now contains details of exactly where the main window is on screen.
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%
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 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.
box_width% = toolbox%!12 - toolbox%!4 : REM Visible Area X1 - X0 box_height% = toolbox%!16 - toolbox%!8 : REM Visible Area Y1 - Y0
As already noted, we know where the main window has ended up on screen. We also know the relationship that we want to have between the main window and its pane, as shown in Figure 2.5. We can therefore update the pane’s window state block so that it is in the correct position relative to the main window, then call Wimp_OpenWindow to open it on the screen.
Figure 2.5: The relationship between the main window and its toolbox
Figure 2.5 shows the relationship between 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 = main%!16 : REM Visible Area Y1 toolbox%!8 = main%!16 - box_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 = main%!4 : REM Visible Area X1 toolbox%!4 = main%!4 - box_width% : REM Visible Area X0
Lost in the stack
The code above deals with the position of the pane in the X and Y dimensions, 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, if it isn’t immediately in front, then 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 was 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, we can open it behind the same window that the main window has just been opened behind. This means that it will end up being inserted into the stack between that window and our main window, thereby ending up in front of our main window as required. To achieve this, we copy the window handle from the main window’s block into the pane’s block.
toolbox%!28 = main%!28
The pane is now ready to be opened in its new location, using a call to Wimp_OpenWindow.
With the pane open in the correct place, there’s nothing else to be done! Putting all of the code above together, PROChandle_pane_windows() will look as shown in Listing 2.6.
DEF PROChandle_pane_windows(main%) LOCAL toolbox%, box_width%, box_height% 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 Find the width and height of the toolbox pane's visible area. box_width% = toolbox%!12 - toolbox%!4 : REM Visible Area X1 - X0 box_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 main window is to go. toolbox%!4 = main%!4 - box_width% : REM Visible Area X0 toolbox%!8 = main%!16 - box_height% : REM Visible Area Y0 toolbox%!12 = main%!4 : REM Visible Area X1 toolbox%!16 = main%!16 : REM Visible Area Y1 REM Open the toolbox pane behind the same window that the main window REM was opened behind. This will place it directly in front of the REM main window. toolbox%!28 = main%!28 SYS "Wimp_OpenWindow",,toolbox% ENDPROC
Listing 2.6: 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.
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 PROChandle_pane_windows(), 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 = main%!4 - box_width% : REM Visible Area X0 toolbox%!8 = main%!16 - box_height% : REM Visible Area Y0 toolbox%!12 = main%!4 : REM Visible Area X1 toolbox%!16 = main%!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:
- If the main window is on-screen by more than the width of the pane, then the pane is anchored by its right-hand edge as before.
- If the main window is on-screen by less than the width of the pane, then the pane is positioned up against the left-hand side of the screen and does not reference its horizontal position to the main window at all.
- If the main window is off off-screen, then the pane is anchored by its left-hand edge.
The code to implement this can be seen in Listing 2.7.
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 main%!4 > box_width% toolbox%!4 = main%!4 - box_width% : REM Visible Area X0 toolbox%!12 = main%!4 : REM Visible Area X1 WHEN main%!4 > 0 toolbox%!4 = 0 : REM Visible Area X0 toolbox%!12 = box_width% : REM Visible Area X1 OTHERWISE toolbox%!4 = main%!4 : REM Visible Area X0 toolbox%!12 = main%!4 + box_width% : REM Visible Area X1 ENDCASE toolbox%!8 = main%!16 - box_height% : REM Visible Area Y0 toolbox%!16 = main%!16 : REM Visible Area Y1
Listing 2.7: 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.2.
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.