10. Modality and what you can click
It is not usually necessary for an inform programmer to consider how an inform program actually works. When one programs in inform, most of what is written is reactive code; one is programming responses to player input
"Underneath the hood", an inform program (at least, one which uses
the standard library) goes through a continuous cycle of asking for
player input (via KeyboardPrimitive
), then responding to it
(for example, by calling before
and after
routines).
The Glk interface works in a similar fashion; one or more events (line input,
character input, mouse input, timer, etc.) are requested, glk_select()
collects these events, then HandleGlkEvent()
is invoked to respond
to the event. In the standard library, KeyboardPrimitive()
and
KeyCharPrimitive()
are where this occurs. GWindows uses the HandleGlkEvent
entry point to invoke the _event hooks.
KeyboardPrimitive
is called by the library once per turn to gather player input. It is at this time that
GWindows does most of its work. If the player enters a character into a window
which is awaiting character input, the char_event
method is invoked
on that window; if the player clicks, the click_event
method is invoked.
KeyboardPrimitive
contains a Glk event loop which calls glk_select
then HandleGlkEvent
over and over until line input is received.
When the player finally types a line into the input window and presses enter,
KeyboardPrimitive
returns, and the processing of the player's input
continues. Until KeyboardPrimitive
is invoked again for the next turn,
GWindows will not process any more input.
Using the library's event loop is fine most of the time; it is the perfect place for GWindows to respond to, for example, the user clicking on a compass, because we can simply use command overrides to "pretend" that the user had actually typed in a command.
However, there may be times when you require some interaction from the player outside of the normal game loop. For example, when displaying a full-screen menu, or showing a splash screen, you really want to suspend the game until the player has pressed a button or made a selection. At these times, you may want to write an event loop of your own.
As described in the previous session, an event loop must have several key components:
glk_select(gg_event)
.
This causes glk to wait for an event to occur, then places
that event in the array gg_event
.
HandleGlkEvent(gg_event, 1, gg_arguments)
,
which will cause GWindows to process the event, for example, by
calling a click_event
method.
One of the most important rules to follow when using your own event loop is to put things back the way you found them; if, during your event loop, you requested some event, make sure that the request is no longer pending when you exit your event loop -- many people have found that their game no longer responded to player input because an event loop they created to pause the game had left a request for character input pending in the input window (Since Glk specifies that you may not request character and line input simultaneously, the library's would be unable to get player input, because the glk_request_line_event would be blocked by the pending character request.).
It is generally preferable to use a built-in event loop than to write your own,
precisely for this reason. If your exit-condition can be made to correspond to a
keypress, then simply calling KeyCharPrimitive
will suffice:
while(KeyCharPrimitive()~='q');
Is an event loop which will execute untill the player presses `q`. The following section describes an even more flexible event-loop.
Normally, GWindows will respond to input in any window that has requested it, just as any active control in a normal application will respond at any time. Consider, however, a window which serves like a "dialog box" in a normal application: the user cannot access any of the application's normal controls until the dialog box closes.
We call such dialogs "modal", because they define a state (or mode) where the user can interact only with that dialog. While overuse of modal dialogs can defeat usability, when used properly, they can be a powerful tool.
In GWindows, we expand the notion of modality to cover event loops which are not part of the standard library: these put the player in a mode where they must deal with some UI element, rather than performing "normal" game interaction.
The GWindows modality package (gmodal.h
) allows you to enter a
modal context, which has its own event loop.
To trigger this event loop, create a modal context by calling GoModal()
. GWindows
will respond to input as it normally would, but attempts by the player to enter commands into
the input window will be ignored.
Unlike the simple example above, the exit condition is left to the author. GoModal
does not return until the program signals that the exit condition has been met.
To do this, call EndModal()
. EndModal
is quite unlike most
other functions in that it does not return a value; any code following a call to EndModal()
inside a function body will not execute (just as statements appearing after a return
,
print_ret
, or quit
will not execute. However, the inform compiler will not
warn you, as it does for these statements). As an example, suppose we wish to wait until
the player clicks at position (0,0) in a certain window:
GImageWin ->
with click_event [x y; if (x==0 && y==0 && GW_Modal) EndModal(); ];
...
print "Now click on the top-left of the image window to continue.^";
GoModal();
print "Thanks!";
While the modal event loop is running, the game will still respond
to being resized, to sound events, and, if there are menus or clickable
windows showing, they will still respond as normal, but the player
will not be able to enter regular input until he has satisfied the request.
It is important in the code above to check that GW_Modal is nonzero, as
it is illegal to call EndModal outside of a modal context (doing so
will trigger the error GW_ERR_NOT_MODAL
). It should be
easy to see how, for example, a modal menu cold be constructed. Once the menu was
activated, call GoModal()
to enter a modal context. The player could then choose
menu options, and finally, selecting the "quit" option would execute EndModal()
.
The line immediately following GoModal
would deactivate the menu.
You can also nest modal contexts; if you call GoModal() while
already in a modal context, you will have to exit first the inner,
then the outer modal context. Alternatively, calling EndModal(1)
will close all open modal contexts immediately. How deeply modal contexts can be nested
is dependent on the Glulx stack.
Within a modal context, you do not need to honor input to the entire UI. If
you wish pop up a menu which does not take up the entire screen, but insist that the
player deal with it before doing anything else, you can call GoModal
with a parameter. If this parameter is a GWindow, only input to that GWindow
will be honored. If it is a window pair, then only input to that subtree of the
UI will be honored. When nesting modal contexts, only the innermost context's
constraint is considered to be active. GoModal(Active_UI)
is exactly
equivalent to calling GoModal
with no parameters.