ARTICLE 8 - UI Menu Primer III
by
HypoThermia
With a good understanding of how the menu system works
(UI Menu Primer I), and a look through the menu reference
(UI Menu Primer II), we're now in a good position
to look at the more advanced features of the menu interface. This article
assume that you've at least read through parts I and II.
If you're contemplating doing some of the things described in this article
then it's assumed that you're savvy enough to be able to work through some
code. Be prepared to look through the source code to improve and polish
your understanding.
Finally, we'll look at some of the things that go into good menu design,
concentrating mainly on pages of controls.
1. Custom menu drawing
Custom menu drawing comes into its own when you're designing a new menu
(or modifying an existing one). When you want to draw a decorative piece of
art or title, then consider using this instead of creating a menu control
that's disabled (doesn't take any input).
There is a limit to the number of controls on a page, if you're reaching
this limit then custom menu drawing might help you cut down on menu controls
that never take input. The alternative is increasing the number of controls
on a page - at the risk of creating excessive clutter.
When it comes to drawing the menu on-screen we can intercept this by setting
the draw function pointer in your menuframework_s structure for that
menu. If this pointer is set then our menu drawing function
will be called instead of the default menu
drawing code. We must remember to call that default code ourselves, otherwise
any controls we've created won't be drawn.
The default menu drawing code is called through DrawMenu(menuframework_s*).
Any background drawing should be done before calling this function, similarly any drawing
done afterward will obscure what has been drawn by DrawMenu().
The menu.draw function must be of the following type:
static void DrawFunctionName(void); // "static" recommended
and assigned when the menu controls are initialized. It's also a good idea to
cache any graphics that'll be drawn.
1.1 Custom menu drawing: Example
The following example shows the bot menu background (a title and left/right brackets)
being drawn without using controls. Taken from ui_addbots.c:
#define ART_BACKGROUND "menu/art/addbotframe"
typedef struct {
menuframework_s menu;
menubitmap_s arrows;
// rest of structure snipped
} addBotsMenuInfo_t;
static addBotsMenuInfo_t addBotsMenuInfo;
/*
=================
UI_AddBotsMenu_Draw
=================
*/
static void UI_AddBotsMenu_Draw( void ) {
UI_DrawBannerString( 320, 16, "ADD BOTS",
UI_CENTER, color_white );
UI_DrawNamedPic( 320-233, 240-166,
466, 332, ART_BACKGROUND );
// standard menu drawing
Menu_Draw( &addBotsMenuInfo.menu );
}
The menu.draw pointer was initialized with the following code
when each of the other controls were initialized:
addBotsMenuInfo.menu.draw = UI_AddBotsMenu_Draw;
2. Updating a menu thats already been pushed
If you create a menu hierarchy with a parent and one or more child menus, you use
UI_PushMenu() to start display of the child, and UI_PopMenu() to
return to the parent. How do you update the parent menu when something
relevent in a child has changed?
You can't rely on the initialization process for the parent, because this is only called
when the menu is created. When control pops to the parent menu it just picks up
from where it was last drawn.
Fortunately you can detect when this happens. The variable uis.firstdraw
is always set to qtrue when a menu page replaces a previously drawn menu. This
happens during either the push that first activates the menu, or the pop that re-activates
the dormant menu. You can check for this during your custom draw routine: either
for an individual control, or the entire menu.
Note that this is late initialization. If you do a lot of data loading
or manipulation as a result then this will cause significant delays in menu
transition. Sometimes you can't avoid this, but you should at least be
aware of it.
This now gives an additional place in which you can initialize data in
controls. It should be used sparingly, and is best for fixing syncronization
problems caused by data changes in child menus.
While checking uis.firstdraw offers one method of updating a dormant
menu, Id have also implemented another. The single player game menu in
ui_splevel.c provides a global function that forces re-initialization,
but deferred until the menu is next drawn. You can track how they did it by
the usage of UI_SPLevelMenu_Reinit() and the variable levelMenuInfo.reinit.
Notice that the use of UI_PopMenu() in this method means that
any info in your menu needs to be saved first.
Use the method you're most comfortable with, or feel is most appropriate.
3. Ownerdrawn controls
What happens if you want something other than the seven standard controls?
Well, short of designing a control of your own from scratch, you can use an
existing control and modify how its drawn on screen. You'd best start
by choosing a control closest in behaviour to what you want. For example:
if you want to draw a clickable picture with some text, then you'd probably start with
the menubitmap_s control (it already draws a picture and is clickable).
When each control is initialized you can set the data member
generic.ownerdraw to point to a function under your control.
By default the body of this ownerdraw function takes FULL responsibility
for drawing the control.
The ownerdraw function must be of the following form:
// self points to the control being drawn
// and can be recast to the generic (menucommon_s*)
// or type of control.
// "static" qualifier recommended
static void OwnerDrawFunction( void *self )
Although there is a QMF_OWNERDRAW flag, it's not actually used. Just
set the function pointer generic.ownerdraw to your
OwnerDrawFunction().
3.1 Fleshing out an ownerdrawn control
If the control can be manipulated by the user then some kind of a "tactile"
feedback is required. This takes the form of a graphical highlight
(indicating one item from a choice), a "pulsing" effect to indicate
focus (that the control is currently selected), or a "greyed out" effect (showing
the control is disabled).
The current state of the control can be accessed by looking at the
state of generic.flags. The following values are tactile and should be
handled as needed:
QMF_GRAYED,QMF_PULSE, QMF_PULSEIFFOCUS,
QMF_HIGHLIGHT, and QMF_HIGHLIGHT_IF_FOCUS.
Controls have special behaviour when implementing the paired flags
(PULSE and HIGHLIGHT). It is also possible for a control to "pulse" or "highlight"
when only the permanent "IFFOCUS" flag is set. This happens when the mouse cursor
is over the control. Example 2.2 below shows how to detect this situation.
Most of the default draw functions for each control has been "hidden" by
making the draw function "static". The two exceptions to this are
Bitmap_Draw(menubitmap_s*) and ScrollList_Draw(menulist_s*),
probably because they're the most complex. Make sure you've initialized your
control so that these draw routines work "as expected".
If you do want to access these "hidden" draw functions then you'll have to remove their
"static" qualifier, and add their declaration to ui_local.h in the section
describing things appearing in ui_qmenu.c.
You can get a lot of good ideas by looking at how the existing 7 controls
are implemented. Take a wander through ui_qmenu.c.
3.2 Ownerdraw example: Handling state flags
Although this is code from one of the 7 provided controls, it shows how
most of the state flags can be handled for the drawing of text. When you're
providing your own drawing routines you'll need to decide which of these you'll
be using, and how to implement them.
This menuaction_s control isn't used within the source code
in its default form. It was supposed to provide a simple menu-like text
control, but was replaced by menutext_s.
For the most part we are just converting the QMF_* flags into the
equivalent UI_* flags, choosing the appropriate colours,
and then calling the text drawing function at the right position on-screen.
If you want to see how state flags can be handled for bitmaps then take
a look at the more complicated Bitmap_Draw() in ui_qmenu.c.
The special consideration of the QMF_PULSEIFFOCUS type of
flag is explained below.
/*
=================
Action_Draw
=================
*/
static void Action_Draw( menuaction_s *a )
{
int x, y;
int style;
float* color;
style = 0;
color = menu_text_color;
if ( a->generic.flags & QMF_GRAYED )
{
color = text_color_disabled;
}
else if (( a->generic.flags & QMF_PULSEIFFOCUS ) &&
( a->generic.parent->cursor ==
a->generic.menuPosition ))
{
color = text_color_highlight;
style = UI_PULSE;
}
else if (( a->generic.flags & QMF_HIGHLIGHT_IF_FOCUS )
&& ( a->generic.parent->cursor ==
a->generic.menuPosition ))
{
color = text_color_highlight;
}
else if ( a->generic.flags & QMF_BLINK )
{
style = UI_BLINK;
color = text_color_highlight;
}
x = a->generic.x;
y = a->generic.y;
UI_DrawString( x, y, a->generic.name,
UI_LEFT|style, color );
if ( a->generic.parent->cursor ==
a->generic.menuPosition )
{
// draw cursor
UI_DrawChar( x - BIGCHAR_WIDTH, y, 13,
UI_LEFT|UI_BLINK, color);
}
}
There are pre-defined colours that correspond to the existing Q3 menu
colour scheme, they can be found in ui_local.h. I'd recommend that
you use them where possible, this makes a colour scheme easier to change.
Notice how the control responds to the QMF_PULSEIFFOCUS flag. It
checks whether the cursor is over that control, and then provides a visual
("tactile") feedback to the user. For completeness it should give
QMF_PULSE the same effect (without checking the cursor).
There is also a function Menu_ItemAtCursor(menuframework_s*)
that returns the current item under the cursor; this can be used in a
similar way, as the following code fragment shows:
// "t" can be any control data structure
menuaction_s* t;
// assign a safe value to "t" before using it
if( Menu_ItemAtCursor( t->generic.parent ) == t ) {
}
// which is identical to this:
if ( t->generic.parent->cursor ==
t->generic.menuPosition ) {
}
3.3 Ownerdraw example: Choose a crosshair
One of the more involved owner drawn controls, this source code is taken
from ui_preferences.c. The usage of this control actually "mixes and
matches" controls and types... so pay attention.
This is essentially a new
type of control. All the drawing and initialization is done outside the
standard seven controls in ui_qmenu.c.
The effect that we're looking for is a control with named text that draws
the cursor graphic. When clicked upon it cycles through all the types of
cursor available.
The first decision was whether a graphic control is modified to draw text
as well, or a text control is modified to draw graphics too. The "conceptual"
idea is a list that draws a graphic (instead of the text choices), so
menubitmap_s wasn't used.
This leads to the control of choice as menulist_s, since we
are selecting one from a list of many:
typedef struct {
// snipped...
menulist_s crosshair;
// snipped...
} preferences_t;
static preferences_t s_preferences;
So far, so good. What follows is where some of the confusion may arise.
When we look at how the crosshair structure is initialized (below),
we see that it's defined as MTYPE_TEXT. This was done with the
QMF_NODEFAULTINIT flag set too, so we have to set the generic parameters ourselves.
This includes the mouse activated area bounded by generic.top, generic.bottom,
generic.left, and generic.right.
Why do things this way? Well this helps while the page of controls is developed.
At the start you have a standard text control that acts as a
"placeholder" for the final owner draw version. You can chop and change the look
of the page until you're happy, without implementing (and possibly breaking)
the owner draw code. You also don't have to set up or use any part specific to the list
control.
Later, when you move to the owner draw implementation,
you don't trigger any of the menulist_s code (there's
no list of text used, so using MTYPE_TEXT makes sure it's never assumed to
be present). Finally, the crosshair.curvalue variable is available
for storing the crosshair type.
Make sure you understand what we have here: this is essentially a new
type of control. All of the drawing and initialization is done outside the
standard seven controls in ui_qmenu.c. So important an idea that it's
worth the second mention.
y = 144;
s_preferences.crosshair.generic.type = MTYPE_TEXT;
s_preferences.crosshair.generic.flags =
QMF_PULSEIFFOCUS|QMF_SMALLFONT|
QMF_NODEFAULTINIT|QMF_OWNERDRAW;
s_preferences.crosshair.generic.x = PREFERENCES_X_POS;
s_preferences.crosshair.generic.y = y;
s_preferences.crosshair.generic.name = "Crosshair:";
s_preferences.crosshair.generic.callback = Preferences_Event;
s_preferences.crosshair.generic.ownerdraw = Crosshair_Draw;
s_preferences.crosshair.generic.id = ID_CROSSHAIR;
s_preferences.crosshair.generic.top = y - 4;
s_preferences.crosshair.generic.bottom = y + 20;
s_preferences.crosshair.generic.left = PREFERENCES_X_POS -
((strlen(s_preferences.crosshair.generic.name)+1)
* SMALLCHAR_WIDTH);
s_preferences.crosshair.generic.right = PREFERENCES_X_POS + 48;
Now that we understand what we have here, this is how the control is drawn
on the screen. Some attention is given to whether the control is greyed or has
the focus. This only applies to the text... not the crosshair graphic.
/*
=================
Crosshair_Draw
=================
*/
static void Crosshair_Draw( void *self ) {
menulist_s *s;
float *color;
int x, y;
int style;
qboolean focus;
s = (menulist_s *)self;
x = s->generic.x;
y = s->generic.y;
style = UI_SMALLFONT;
focus = (s->generic.parent->cursor ==
s->generic.menuPosition);
if ( s->generic.flags & QMF_GRAYED )
color = text_color_disabled;
else if ( focus )
{
color = text_color_highlight;
style |= UI_PULSE;
}
else if ( s->generic.flags & QMF_BLINK )
{
color = text_color_highlight;
style |= UI_BLINK;
}
else
color = text_color_normal;
if ( focus )
{
// draw cursor
UI_FillRect( s->generic.left, s->generic.top,
s->generic.right-s->generic.left+1,
s->generic.bottom-s->generic.top+1,
listbar_color );
UI_DrawChar( x, y, 13, UI_CENTER|UI_BLINK|
UI_SMALLFONT, color);
}
UI_DrawString( x - SMALLCHAR_WIDTH, y, s->generic.name,
style|UI_RIGHT, color );
if( !s->curvalue ) {
return;
}
UI_DrawHandlePic( x + SMALLCHAR_WIDTH, y - 4, 24, 24,
s_preferences.crosshairShader[s->curvalue] );
}
Notice the use of s->curvalue (in the last line of code)
to decide which crosshair to draw. The following code snippet is from the
event handler for the control. We have to update and wrap curvalue
for the correct behaviour when the control is clicked on. This would be done
for you in a menulist_s correctly identified as
MTYPE_SPINCONTROL.
static void Preferences_Event( void* ptr, int notification ) {
if( notification != QM_ACTIVATED ) {
return;
}
switch( ((menucommon_s*)ptr)->id ) {
case ID_CROSSHAIR:
s_preferences.crosshair.curvalue++;
if( s_preferences.crosshair.curvalue == NUM_CROSSHAIRS ) {
s_preferences.crosshair.curvalue = 0;
}
trap_Cvar_SetValue( "cg_drawCrosshair",
s_preferences.crosshair.curvalue );
break;
// code snipped...
}
}
4. Using the status bar
If a control has any special behaviour, or takes special value(s), then consider
using a statusbar to inform/remind the user. This additional information
about a control will be displayed when the cursor hovers over the control, and makes
use of the generic.statusbar callback function.
static void StatusBarFunction( void *self )
The pointer to self indicates the control that is drawing the
status bar. If you don't have access to the source control then
re-cast the pointer to (menucommon_s*) for the type of control in
self->type.
The status bar function is repeatedly called each time the control is drawn
(which is as quickly as your graphics drivers will allow), and only if the cursor is over it;
so you shouldn't set anything permanent. The information drawn is usually
placed at the bottom of the screen.
Use of the status bar function saves on creating a text control, and
also avoids invoking an ownerdrawn version of the control to detect the presence
of the cursor. Naturally, the area of the screen used by the status bar
text should be kept free of other controls.
4.1 Status bar example
This example of a statusbar is taken from ui_startserver2.c where
a single player game is setup. It's displayed when the cursor is over the
timelimit or fraglimit controls. In this case the same status bar function is
used for both controls.
/*
=================
ServerOptions_StatusBar
=================
*/
static void ServerOptions_StatusBar( void* ptr ) {
UI_DrawString( 320, 440, "0 = NO LIMIT",
UI_CENTER|UI_SMALLFONT, colorWhite );
}
The code for initializing the control looks like this:
s_serveroptions.fraglimit.generic.type = MTYPE_FIELD;
s_serveroptions.fraglimit.generic.name = "Frag Limit:";
s_serveroptions.fraglimit.generic.flags =
QMF_NUMBERSONLY|QMF_PULSEIFFOCUS|QMF_SMALLFONT;
s_serveroptions.fraglimit.generic.x = OPTIONS_X;
s_serveroptions.fraglimit.generic.y = y;
s_serveroptions.fraglimit.generic.statusbar =
ServerOptions_StatusBar;
s_serveroptions.fraglimit.field.widthInChars = 3;
s_serveroptions.fraglimit.field.maxchars = 3;
5. Designing the menu
I'm not going to turn this into (much of) a lecture on good design: you don't want to
read that, and I'm not really qualified to give it. What I'll do is point
out a few things that Id have done,
how you can benefit from doing similar things, and that people do take an
impression away with them about good or bad interface design.
'Nuf arm twisting.
5.1 The good, the bad, and the just plain ugly
There are several things that jump out about the way Id have designed
their menus. The most obvious thing is that they're relatively uncluttered,
with a fair amount of open space.
Take the two skirmish menu pages that help you setup a single or
multi-player game. They could have been squeezed onto one page... one complicated
and tightly packed page. Instead they've been split up over two pages, with controls
grouped together (and separate from each other) so you can see what's related to what.
The first skirmish page chooses the map and type of game. For team games the second page
will add red/blue options for each of the bots/players. If this were all done
on one page then there'd be the added confusion of these fields vanishing and
re-appearing as the game type was cycled. Not exactly eye candy, now is it?
For pages of controls that follow on from each other, there are always
buttons that take you forward and take you back. The button that takes
you back is in the lower left corner, while the one that takes you
forward is in the bottom right corner. Think of page turning in a book to
see the sense in this. You don't read books? Then think of the forward/back
buttons on your web browser.
Related controls are almost always close together. This gives two benefits:
you show that they are related (duh!), and when they're changed the user needs
to move only a short distance to neighbours. Moving a long distance between
frequently changed controls is a pain in the wrist! You want to save your wrist
something else... right?
When choosing one action makes another control redundant, then that redundant
control is hidden, disabled, or greyed out. This helps the user understand that
something isn't going to do anything. And lets face it, users like you for taking
decisions like that away from them.
5.2 Back to pencil and paper
Just a quick tip for those who are contemplating a new menu page full
of controls. I've found that you can design a 640x480 screen of controls on
an A4 page of squared paper (the kind that has squares about 5mmx5mm, you
probably did maths sums in school using paper like this).
If you take each square as 16x16 pixels then the page is just about filled.
The small font gets two characters per square, the standard big font get one char
per square. Screen co-ordinates are then easily and accurately recovered, and you
have a hard copy of what you want the final menu page to look like.
You can move controls around easily, and quickly see what does
and doesn't work. Once you've coded something there's a lot of effort
that might have to go to waste.
6. Congratulations...
Phew! If you've got this far then you've read through a lot of stuff about
writing menu code. Thanks for the perseverance. I hope it's given you the infomation
you needed.
If you've followed and understood then you should be able to write exactly
what you need for your mod in the way of menu code. Let me know if there's
anything that needs clarifying, or if I've missed something major.
Cheers!
HypoThermia
|