HELP PWMWINIO                               Roger Evans, October 1987

Character-stream input and output on PWM user-defined windows.


         CONTENTS - (Use <ENTER> g to access required sections)

 -- Introduction
 -- The PWM event philosophy
 -- Stream-based interaction in user windows
 -- Some additional design points
 -- The example code
 -- Application notes


-- Introduction -------------------------------------------------------

This help file discusses a technique for implementing character-stream
input and output on PWM user-defined windows using the POP-11 process
mechanism, and provides some example code.

The discussion assumes some familiarity with topics discussed in the
following HELP files:
    *PWMINPUT *PWMWINDOWS *PROCESS *CUCHARIN *CUCHAROUT *DLOCAL

However, the example code is self-contained and may be of use directly
without detailed knowledge of the techniques employed. But it should be
noted that this code is NOT a supported POPLOG library.

-- The PWM event philosophy -------------------------------------------

In common with most windowing systems, the PWM adopts an 'event driven'
view of user input. It provides facilities, described in *PWMINPUT, for
specifying that when an event of a given type is received on a given
window a particular procedure is to be run. This allows one to specify a
wide range of different responses to mouse presses, keyboard input etc.
on a per-window basis, and encourages a style of interaction in which
the user can treat each window as an independent context in which some
task is being undertaken, and can switch freely between them at any time
(even half way through an interaction) by moving the mouse from one to
another.

One way to write programs in Poplog which fit in with this model is to
arrange that each window/task has its own 'private' piece of the global
Poplog environment and service each event with a piece of code which
alters some part of it, typically fairly quickly so that the user is not
held up waiting for one task to complete something when in fact she
wants to switch to some other task. This is basically the way VED
operates within the PWM - each VED window has its own global data (the
underlying VED buffer) which gets modified by key presses in that
window. The user is free to switch from window to window at any
point, except when she has asked VED to do something which takes some
time (loading a file for example), in which case she must wait until
it is finished before editing in some other window.

However, there are a range of applications for which this set-up is
unsatisfactory. Many conventional tasks employ a stream-based model of
user interaction. This model assumes that a program will simply run
until it requires user input, at which point it will stop and wait for
as long as necessary until the input is provided, and then carry on with
whatever it wants to do. The POP-11 'top-level' loop is an obvious
example of this programming style - POP-11 waits indefinitely for the
user to type commands, then executes them, taking as long as necessary
to do it, and then comes back to read more commands.

In the PWM, it is possible to have stream-based interaction of this sort
in the base window and still be able to switch to other tasks in other
windows (eg VED windows). However, the facilities provided by the base
window are somewhat limited. In particular, it is not possible to handle
arbitrary events (for example mouse buttons) and only one stream-based
task can be running at a time. The remainder of this help file describes
a way of providing stream-based interaction facilities in user-created
windows, thereby overcoming both of these difficulties.


-- Stream-based interaction in user windows ---------------------------

We assume that we have some procedure -proc- which employs a
stream-based interaction style using -cucharin- and -cucharout-, and we
want to run it in a user-defined window -window- and to maintain a
basically event-driven interaction style (with the user able to switch
from window to window at will, etc.). Consider what happens when we run
-proc-: whenever -proc- is engaged in processing there is nothing we can
do - Poplog as a whole can only do one thing at once. But when -proc-
decides to read some user input, we want the user to be able to abandon
-proc- and go and do something else (in a different window, say), and
then to be able to come back at some arbitrary time in the future and
carry on. So there are three distinct new facilities that must be
provided:

    1)  it must be possible to interrupt the execution of -proc- and
        restart it at a later time
    2)  whenever -proc- goes into a read, it must be possible for the
        user to choose to abandon -proc- (using (1)) and go and do
        something else
    3)  it must be possible for the user to choose to restart -proc- and
        carry on with the read

Poplog already provides a facility to achieve (1), namely the POP-11
process mechanism (see *PROCESS). Then, if -proc- is running as a
process, (2) can be achieved by making -proc- suspend (see *SUSPEND)
every time it wants to read, and providing facilities outside of -proc-
for choosing what to do next. (3) then becomes a special case of 'what
to do next' - whenever (and it may be straight way) the user decides to
provide some input for -proc- (by typing input into -window-), -proc-
gets restarted. Furthermore, the facilities for choosing what to do next
are already available in the PWM in the form of the event handling
mechanism.

Thus the basic structure of our approach is as follows. -proc- is run
inside a process, rather than just as an ordinary procedure. This
process locally defines -cucharin- to suspend, allowing control to
return to the main Poplog top-level process. We define the event handler
for events on -window- to restart the process, so that we are back
inside -cucharin-, which can decode the event and return a character as
required. In addition the process locally defines -cucharout- to send
output to -window-, but this is fairly straightforward.

The net effect of this set-up operates roughly like this. A process is
created which localises -cucharin- and -cucharout- and then runs -proc-.
-proc- processes as it wishes until it wants to read some input. It then
calls -cucharin- which suspends the process. The user is now free to
interact with Poplog as she wishes, editing VED buffers, using the base
window, other user windows etc.. But as soon as she types a character
into -window-, the event handler restarts the process, passing in the
character event recieved. When the process restarts, -cucharin- receives
the event, extracts the character and returns it. As far as -proc- is
concerned, all that happened was that it called -cucharin- and
-cucharin- returned with a character. -proc- then just continues
processing until it calls -cucharin- again, at which point the user once
more regains control.


-- Some additional design points --------------------------------------

The code provided below follows the basic design described in the
preceding section. First, however, there are some additional points
relating to this design which should be noted.

1)  Echoing and input editing

The facilities so far described will cause -cucharin- to return each
character typed immediately without prompting, or echoing the character
on the screen (so the user will not see what she is typing) or giving
the user the chance to edit input (with the <DELETE> key, for example)
before -proc- sees it. Thus -cucharin- behaves much like -rawcharin-. If
a more friendly -charin-like interface is required, this has to be
handled explicitly, either by the event handler routine, or by
-cucharin- itself. The example code does the latter, providing a version
of -cucharin- which prompts, echoes, and buffers input until a whole
line has been read, allowing editing using <DELETE>. It could be
extended to allow CTRL-U (delete whole line) and other common editing
functions.

2)  PWM event handling

The implementation depends crucially on the facilities provided by the
PWM event handler. These are only available if the main Poplog process
is either in VED or doing a 'macro-expanding read' (typically a call of
-itemread-). In most simple cases, this means that Poplog's base window
must be sitting in a top-level language loop (POP-11, Prolog, Common
Lisp etc.). If a user application program is running in the base window
and waiting for input but NOT doing macro-expansion, then event handling
will not take place (indeed input to any window, including VED windows,
will be seen by the user application, rather than its intended target
task).

3)  Garbage collection of windows

Whenever Poplog's garbage collector encounters a window which is garbage
(that is, there are no user-accessible references to it), it kills it.
In normal circumstances this is desirable, since if the user cannot
access it, it can't be much use. However, in order for this mechanism to
work smoothly, the garbage collector ignores references to windows in
the event handler routines (because all windows always have such
references, and so this convenient window killing would never take
place). This causes a slight problem for the design proposed above,
because it is possible to lose all references to -window- except those
accessed via the event handler routines, and yet still want the window
to stay alive. This is because once -cucharin- has suspended, the ONLY
way to restart it may be to type something to -window-, and in that
case, nothing outside of the process need know about -window-, and
process need only be referred to from the event handler routines. In
such a situation, the window in which the process is quite happily
waiting for input may suddenly die for no apparent reason when Poplog
does a garbage collection. (The process itself will typically die on the
next garbage collection.)

To overcome this problem, it is recommended that applications maintain
an independent reference to all the windows using this stream-based
interface. The code below deliberately returns the window data structure
of the window used to make this possible. In order to ensure the window
does not get killed accidentally, this window structure must be saved
somewhere (in a global variable, or a list of such windows etc. - it
doesn't matter at all where the reference to it is)


-- The example code ---------------------------------------------------

The code defined in this section employs the techniques discussed above
to provide a utility, -winio- for executing a procedure in a context in
which -cucharin- accepts characters typed to a user-defined window and
-cucharout- displays characters in the same window. -winio- can be
called in several formats, depending on whether you want it to create a
window for you or use an existing one, and whether you want it to prompt
and echo (like -charin-) or not (like -rawcharin-).

    winio(title,width,height,proc) -> process -> window;

This call will create a window (using -pwm_make_txtwin- on the first
three arguments) and run the procedure -proc- inside a process with
-cucharin- and -cucharout- suitably defined. It returns the process
created (so that, for example, it can be killed cleanly if desired), and
the window structure, (so that the garbage collection problems discussed
above can be avoided by saving the window in some global variable or
whatever).

    winio(window,proc) -> process -> window;

This call uses the window specified (mishapping if it is not a PWM text
window) to run the procedure in, and so is useful in cases where the
window already exists. It returns the same window structure it was
given.

    winio(title,width,height,proc,mode) -> process -> window;
    winio(window,proc,mode) -> process -> window;

These two calls include an extra -mode- argument which specifies how
-cucharin- should behave. If -mode- is <true>, -cucharin- does no
echoing and returns each character immediately (like -rawcharin-), if
-mode- is <false> (the default if not specified), -cucharin- prompts for
input, echoes characters, allows the use of the <DELETE> key, and
returns no characters until <RETURN> or <LINE-FEED> is typed (cf. mode
argument in *SYSCREATE). -mode- can also be a string, in which case it
prompts, echoes etc., using the string supplied as prompt (with -mode-
set to <false>, the current value of -popprompt- is used as the prompt).



/* -- WINIO: start of code ------------------------------------ */

uses poppwmlib;

/* length of buffer for buffered reads */
constant winio_bufflen = 256;

/*  The procedure that is run as a process.
    -cucharin- is locally defined - 'echo' and 'noecho' versions are
    defined and -mode- is used to select between them.
    -cucharout- is just rawcharout, but localisation of -pwmtextwindow-
    ensures the right window gets used in the context of this process.
*/
define winio_process(proc,window,mode);
    lvars proc window mode c;
    lvars len=0, ptr=1, buff=inits(winio_bufflen);
    dlocal pwmtextwindow cucharin cucharout;

    /*  echo version: read a whole line into a buffer and then
        let them out one at a time */
    define winio_echocharin;
        lvars c vector;

        /* read in a line of data if there's none left */
        unless len >= ptr do
            /* prompt as required */
            appdata(if isstring(mode) then mode else popprompt endif,
                    rawcharout);
            sysflush(popdevraw);

            1 ->> ptr -> len;
            repeat;
                /* suspend until another character is available */
                suspend(0) -> vector;
                subscrv(2,vector) -> c;
                /* echo and save, handling special chars (CR,LF,DEL) */
                if c == `\r` then `\n` -> c endif;
                if c == `\^?` then
                    /* delete last char if there are any left */
                    if len > 1 then
                        rawcharout(c);
                        len - 1 -> len;
                    endif;
                elseif c == `\n` or c == `\^Z` then
                    /* end of line */
                    c -> subscrs(len,buff);
                    rawcharout(c);
                    quitloop
                else
                    /* ordinary char */
                    c -> subscrs(len,buff);
                    rawcharout(c);
                    len + 1 -> len;
                    /* check for full buffer - treat it as whole line */
                    if len > winio_bufflen then quitloop endif;
                endif;
                sysflush(popdevraw);
            endrepeat;
            sysflush(popdevraw);
        endunless;

        /* get next data char, translating ctrl Z to termin */
        subscrs(ptr,buff) -> c;
        ptr + 1 -> ptr;
        if c == `\^Z` then termin else c endif;
    enddefine;

    /* noecho version: suspend until next char arrives and return it */
    define winio_rawcharin;
        lvars c;
        subscrv(2,suspend(0)) -> c;
        if c == `\^Z` then termin else c endif;
    enddefine;

    /* assign io channels ... */
    window -> pwmtextwindow;
    rawcharout -> cucharout;
    if mode == true then
        winio_rawcharin
    else
        winio_echocharin
    endif -> cucharin;

    /* ... and run the given procedure */
    proc();
enddefine;

/*  winio itself - slightly involved handling of arguments, but
    after than, just make the process, set the character handler
    to restart it every character, and set it going (it will suspend,
    and winio will return, as soon as -proc- tries to do input)
*/
define winio(proc) -> process -> window;
    lvars window proc process type mode;

    /* ---- arg checking ---- */

    /* look for optional mode specification */
    if isstring(proc) or isboolean(proc) then
        proc -> mode; -> proc;
    else
        false -> mode;
    endif;

    /* check that procedure really is one */
    unless isprocedure(proc) then
        mishap(proc,1,'non-procedure given to winio');
    endunless;

    /* check window - user supplied or args to make one? */
    -> window;
    if pwm_windowtype(window) ->> type then
        unless type == "text" then
            mishap(window,1,'bad window given to winio');
        endunless;
    else
        /*  assume window is last arg to pwm_maketextwin and that
            other args are on stack */
        unless pwm_make_txtwin(window) ->> window then
            mishap(0,'can\'t make window in winio');
        endunless;
    endif;

    /* ---- make process and set handler ---- */
    consproc(proc,window,mode,3,winio_process) -> process;

    /*  character handler */
    procedure(vector);
        /* check for live process - ignore input if not */
        if isliveprocess(process) then
            /* restart process passing in character event */
            runproc(vector,1,process);
        endif;
    endprocedure -> pwm_eventhandler(window,"character");

    /*  start the window process running */
    runproc(0,process);
enddefine;

/* -- WINIO: end of code -------------------------------------- */

-- Application notes --------------------------------------------------

1.  If -proc- invokes -readline- (see *READLINE) it will work only if
    the original -winio- was called from top-level or VED immediate
    mode. If it was called with 'load marked range' strange effects may
    occur.

2.  No handling of mishaps or -cucharerr- is provided - be warned!

3.  To use this mechanism to invoke a Prolog goal, say foo(a,b,c),
    a suitable procedure argument to -winio- is:

        prolog_barrier_apply(%
            prolog_invoke(%prolog_maketerm("a","b","c","foo",3)%)%)

    Note however, that foo must invoke

       see(inchan)

    to make Prolog's read pick up -cucharin- (rather than -charin-).
     Also, this mechanism will currently only work if Prolog is NOT the
    'current' language subsystem. That is, the top-level from which
    -winio- gets called should be POP-11 or CLISP.

4.  The PWM library -pwm_get_selection- makes it easy to implement a
    simple 'stuff' mechanism using -winio-. The following code makes the
    pressing of button 2 stuff the current selected text into the user
    window (where it is then seen by the -winio- process).

        pwm_maketxwindow(title,width,height) -> window;

        procedure(v);
            if v(2) == 2 then
                /* pwm_get_selection triggers events for all the
                   characters in the selection */
                pwm_get_selection();
            endif;
        endprocedure -> pwm_eventhandler(window,"press");

        winio(window,proc) -> process -> window;

5.  If the process wants to handle events other than characters the code
    can easily be extended to make this possible. The simplest approach
    is to assign the same handler procedure to other event-types (eg
    "press") and then carry out more tests on the vector returned by
    'suspend' in -winio_echocharin- and -winio_rawcharin-.

6.  The implementation of buffered input is less efficient than it could
    be, because the -winio- process gets restarted on every character. A
    more efficient approach would be to handle the buffering in the
    event handler procedure, and only restart the process when a
    whole line had been read, passing the complete line in to
    -winio_echocharin-.

--- C.pwm/help/pwmwinio ------------------------------------------------
--- Copyright University of Sussex 1987. All rights reserved. ----------
